feat(ui): Sonarr support for the series details popup

This commit is contained in:
2024-12-06 20:30:26 -07:00
parent 73d666d1f5
commit 23b1ca4371
39 changed files with 3075 additions and 956 deletions
Generated
+20
View File
@@ -489,12 +489,30 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "derive_setters"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8ef033054e131169b8f0f9a7af8f5533a9436fadf3c500ed547f730f07090d"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.89",
]
[[package]] [[package]]
name = "destructure_traitobject" name = "destructure_traitobject"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7"
[[package]]
name = "deunicode"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00"
[[package]] [[package]]
name = "diff" name = "diff"
version = "0.1.13" version = "0.1.13"
@@ -1319,6 +1337,8 @@ dependencies = [
"crossterm", "crossterm",
"ctrlc", "ctrlc",
"derivative", "derivative",
"derive_setters",
"deunicode",
"dirs-next", "dirs-next",
"human-panic", "human-panic",
"indicatif", "indicatif",
+2
View File
@@ -50,6 +50,8 @@ async-trait = "0.1.83"
dirs-next = "2.0.0" dirs-next = "2.0.0"
managarr-tree-widget = "0.24.0" managarr-tree-widget = "0.24.0"
indicatif = "0.17.9" indicatif = "0.17.9"
derive_setters = "0.1.6"
deunicode = "1.6.0"
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0.16" assert_cmd = "2.0.16"
+7 -1
View File
@@ -196,7 +196,13 @@ impl<'a> App<'a> {
.current_selection() .current_selection()
.clone() .clone()
.seasons .seasons
.unwrap_or_default(); .unwrap_or_default()
.into_iter()
.map(|mut season| {
season.title = Some(format!("Season {}", season.season_number));
season
})
.collect();
self.data.sonarr_data.seasons.set_items(seasons); self.data.sonarr_data.seasons.set_items(seasons);
} }
} }
+18
View File
@@ -53,6 +53,24 @@ pub static HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.esc, "cancel filter"), (DEFAULT_KEYBINDINGS.esc, "cancel filter"),
]; ];
pub static SERIES_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
(
DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc,
),
(DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
];
pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [ pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
( (
DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh,
+52 -1
View File
@@ -9,7 +9,7 @@ mod tests {
HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES,
SEASON_DETAILS_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES,
SYSTEM_TASKS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
}, },
}; };
@@ -86,6 +86,57 @@ mod tests {
assert_eq!(series_context_clues_iter.next(), None); assert_eq!(series_context_clues_iter.next(), None);
} }
#[test]
fn test_series_history_context_clues() {
let mut series_history_context_clues_iter = SERIES_HISTORY_CONTEXT_CLUES.iter();
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.update.desc);
let (key_binding, description) = series_history_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel filter/close");
assert_eq!(series_history_context_clues_iter.next(), None);
}
#[test] #[test]
fn test_history_context_clues() { fn test_history_context_clues() {
let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter(); let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter();
+15 -1
View File
@@ -1,7 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
mod sonarr_tests { mod sonarr_tests {
use pretty_assertions::assert_eq; use pretty_assertions::{assert_eq, assert_str_eq};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::{ use crate::{
@@ -572,6 +572,13 @@ mod tests {
app.populate_seasons_table().await; app.populate_seasons_table().await;
assert!(!app.data.sonarr_data.seasons.items.is_empty()); assert!(!app.data.sonarr_data.seasons.items.is_empty());
assert_str_eq!(
app.data.sonarr_data.seasons.items[0]
.title
.as_ref()
.unwrap(),
"Season 0"
);
} }
#[tokio::test] #[tokio::test]
@@ -585,6 +592,13 @@ mod tests {
app.populate_seasons_table().await; app.populate_seasons_table().await;
assert!(!app.data.sonarr_data.seasons.items.is_empty()); assert!(!app.data.sonarr_data.seasons.items.is_empty());
assert_str_eq!(
app.data.sonarr_data.seasons.items[0]
.title
.as_ref()
.unwrap(),
"Season 0"
);
} }
fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver<NetworkEvent>) { fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver<NetworkEvent>) {
+5
View File
@@ -323,6 +323,11 @@ mod test_utils {
macro_rules! test_handler_delegation { macro_rules! test_handler_delegation {
($handler:ident, $base:expr, $active_block:expr) => { ($handler:ident, $base:expr, $active_block:expr) => {
let mut app = App::default(); let mut app = App::default();
let mut series_history = $crate::models::stateful_table::StatefulTable::default();
series_history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
]);
app.data.sonarr_data.series_history = Some(series_history);
app.push_navigation_stack($base.into()); app.push_navigation_stack($base.into());
app.push_navigation_stack($active_block.into()); app.push_navigation_stack($active_block.into());
@@ -457,7 +457,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
_ if key == DEFAULT_KEYBINDINGS.refresh.key => { _ if key == DEFAULT_KEYBINDINGS.refresh.key => {
self self
.app .app
.pop_and_push_navigation_stack((self.active_radarr_block).into()); .pop_and_push_navigation_stack(self.active_radarr_block.into());
} }
_ if key == DEFAULT_KEYBINDINGS.sort.key => { _ if key == DEFAULT_KEYBINDINGS.sort.key => {
self self
@@ -1785,6 +1785,7 @@ mod tests {
let mut app = App::default(); let mut app = App::default();
app.is_loading = true; app.is_loading = true;
app.push_navigation_stack(active_radarr_block.into()); app.push_navigation_stack(active_radarr_block.into());
app.is_routing = false;
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal { app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal {
movie_details: ScrollableText::with_string("test".to_owned()), movie_details: ScrollableText::with_string("test".to_owned()),
..MovieDetailsModal::default() ..MovieDetailsModal::default()
@@ -1799,7 +1800,7 @@ mod tests {
.handle(); .handle();
assert_eq!(app.get_current_route(), active_radarr_block.into()); assert_eq!(app.get_current_route(), active_radarr_block.into());
assert!(app.is_routing); assert!(!app.is_routing);
} }
#[rstest] #[rstest]
+1 -1
View File
@@ -312,7 +312,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, '
} }
} }
fn history_sorting_options() -> Vec<SortOption<SonarrHistoryItem>> { pub(in crate::handlers::sonarr_handlers) fn history_sorting_options() -> Vec<SortOption<SonarrHistoryItem>> {
vec![ vec![
SortOption { SortOption {
name: "Source Title", name: "Source Title",
@@ -11,9 +11,7 @@ mod tests {
use crate::event::Key; use crate::event::Key;
use crate::handlers::sonarr_handlers::library::{series_sorting_options, LibraryHandler}; use crate::handlers::sonarr_handlers::library::{series_sorting_options, LibraryHandler};
use crate::handlers::KeyEventHandler; use crate::handlers::KeyEventHandler;
use crate::models::servarr_data::sonarr::sonarr_data::{ use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS, DELETE_SERIES_BLOCKS, EDIT_SERIES_BLOCKS, LIBRARY_BLOCKS, SERIES_DETAILS_BLOCKS};
ActiveSonarrBlock, ADD_SERIES_BLOCKS, DELETE_SERIES_BLOCKS, EDIT_SERIES_BLOCKS, LIBRARY_BLOCKS,
};
use crate::models::sonarr_models::{Series, SeriesStatus, SeriesType}; use crate::models::sonarr_models::{Series, SeriesStatus, SeriesType};
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
use crate::models::HorizontallyScrollableText; use crate::models::HorizontallyScrollableText;
@@ -615,6 +613,7 @@ mod tests {
#[test] #[test]
fn test_search_series_submit() { fn test_search_series_submit() {
let mut app = App::default(); let mut app = App::default();
app.should_ignore_quit_key = true;
app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.push_navigation_stack(ActiveSonarrBlock::SearchSeries.into()); app.push_navigation_stack(ActiveSonarrBlock::SearchSeries.into());
app app
@@ -629,6 +628,7 @@ mod tests {
LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchSeries, None).handle(); LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchSeries, None).handle();
assert!(!app.should_ignore_quit_key);
assert_str_eq!( assert_str_eq!(
app.data.sonarr_data.series.current_selection().title.text, app.data.sonarr_data.series.current_selection().title.text,
"Test 2" "Test 2"
@@ -639,6 +639,7 @@ mod tests {
#[test] #[test]
fn test_search_series_submit_error_on_no_search_hits() { fn test_search_series_submit_error_on_no_search_hits() {
let mut app = App::default(); let mut app = App::default();
app.should_ignore_quit_key = true;
app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.push_navigation_stack(ActiveSonarrBlock::SearchSeries.into()); app.push_navigation_stack(ActiveSonarrBlock::SearchSeries.into());
app app
@@ -653,6 +654,7 @@ mod tests {
LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchSeries, None).handle(); LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchSeries, None).handle();
assert!(!app.should_ignore_quit_key);
assert_str_eq!( assert_str_eq!(
app.data.sonarr_data.series.current_selection().title.text, app.data.sonarr_data.series.current_selection().title.text,
"Test 1" "Test 1"
@@ -666,6 +668,7 @@ mod tests {
#[test] #[test]
fn test_search_filtered_series_submit() { fn test_search_filtered_series_submit() {
let mut app = App::default(); let mut app = App::default();
app.should_ignore_quit_key = true;
app app
.data .data
.sonarr_data .sonarr_data
@@ -685,6 +688,7 @@ mod tests {
LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchSeries, None).handle(); LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchSeries, None).handle();
assert!(!app.should_ignore_quit_key);
assert_str_eq!( assert_str_eq!(
app.data.sonarr_data.series.current_selection().title.text, app.data.sonarr_data.series.current_selection().title.text,
"Test 2" "Test 2"
@@ -695,6 +699,7 @@ mod tests {
#[test] #[test]
fn test_filter_series_submit() { fn test_filter_series_submit() {
let mut app = App::default(); let mut app = App::default();
app.should_ignore_quit_key = true;
app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.push_navigation_stack(ActiveSonarrBlock::FilterSeries.into()); app.push_navigation_stack(ActiveSonarrBlock::FilterSeries.into());
app app
@@ -732,6 +737,7 @@ mod tests {
#[test] #[test]
fn test_filter_series_submit_error_on_no_filter_matches() { fn test_filter_series_submit_error_on_no_filter_matches() {
let mut app = App::default(); let mut app = App::default();
app.should_ignore_quit_key = true;
app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::Series.into());
app.push_navigation_stack(ActiveSonarrBlock::FilterSeries.into()); app.push_navigation_stack(ActiveSonarrBlock::FilterSeries.into());
app app
@@ -946,7 +952,6 @@ mod tests {
} }
mod test_handle_key_char { mod test_handle_key_char {
use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use serde_json::Number; use serde_json::Number;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
@@ -1465,27 +1470,30 @@ mod tests {
); );
} }
// #[rstest] #[rstest]
// fn test_delegates_series_details_blocks_to_series_details_handler( fn test_delegates_series_details_blocks_to_series_details_handler(
// #[values( #[values(
// ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesDetails,
// ActiveSonarrBlock::SeriesHistory, ActiveSonarrBlock::SeriesHistory,
// ActiveSonarrBlock::FileInfo, ActiveSonarrBlock::SearchSeason,
// ActiveSonarrBlock::Cast, ActiveSonarrBlock::SearchSeasonError,
// ActiveSonarrBlock::Crew, ActiveSonarrBlock::UpdateAndScanSeriesPrompt,
// ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, ActiveSonarrBlock::AutomaticallySearchSeriesPrompt,
// ActiveSonarrBlock::UpdateAndScanPrompt, ActiveSonarrBlock::SearchSeriesHistory,
// ActiveSonarrBlock::ManualSearch, ActiveSonarrBlock::SearchSeriesHistoryError,
// ActiveSonarrBlock::ManualSearchConfirmPrompt ActiveSonarrBlock::FilterSeriesHistory,
// )] ActiveSonarrBlock::FilterSeriesHistoryError,
// active_sonarr_block: ActiveSonarrBlock, ActiveSonarrBlock::SeriesHistorySortPrompt,
// ) { ActiveSonarrBlock::SeriesHistoryDetails
// test_handler_delegation!( )]
// LibraryHandler, active_sonarr_block: ActiveSonarrBlock,
// ActiveSonarrBlock::Series, ) {
// active_sonarr_block test_handler_delegation!(
// ); LibraryHandler,
// } ActiveSonarrBlock::Series,
active_sonarr_block
);
}
#[rstest] #[rstest]
fn test_delegates_edit_series_blocks_to_edit_series_handler( fn test_delegates_edit_series_blocks_to_edit_series_handler(
@@ -1711,6 +1719,7 @@ mod tests {
library_handler_blocks.extend(ADD_SERIES_BLOCKS); library_handler_blocks.extend(ADD_SERIES_BLOCKS);
library_handler_blocks.extend(DELETE_SERIES_BLOCKS); library_handler_blocks.extend(DELETE_SERIES_BLOCKS);
library_handler_blocks.extend(EDIT_SERIES_BLOCKS); library_handler_blocks.extend(EDIT_SERIES_BLOCKS);
library_handler_blocks.extend(SERIES_DETAILS_BLOCKS);
ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { ActiveSonarrBlock::iter().for_each(|active_sonarr_block| {
if library_handler_blocks.contains(&active_sonarr_block) { if library_handler_blocks.contains(&active_sonarr_block) {
@@ -22,6 +22,7 @@ use crate::{
use super::handle_change_tab_left_right_keys; use super::handle_change_tab_left_right_keys;
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler;
mod add_series_handler; mod add_series_handler;
mod delete_series_handler; mod delete_series_handler;
@@ -29,6 +30,7 @@ mod delete_series_handler;
#[cfg(test)] #[cfg(test)]
#[path = "library_handler_tests.rs"] #[path = "library_handler_tests.rs"]
mod library_handler_tests; mod library_handler_tests;
mod series_details_handler;
pub(super) struct LibraryHandler<'a, 'b> { pub(super) struct LibraryHandler<'a, 'b> {
key: Key, key: Key,
@@ -51,6 +53,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, '
EditSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context) EditSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context)
.handle(); .handle();
} }
_ if SeriesDetailsHandler::accepts(self.active_sonarr_block) => {
SeriesDetailsHandler::with(self.key, self.app, self.active_sonarr_block, self.context)
.handle();
}
_ => self.handle_key_event(), _ => self.handle_key_event(),
} }
} }
@@ -59,6 +65,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, '
AddSeriesHandler::accepts(active_block) AddSeriesHandler::accepts(active_block)
|| DeleteSeriesHandler::accepts(active_block) || DeleteSeriesHandler::accepts(active_block)
|| EditSeriesHandler::accepts(active_block) || EditSeriesHandler::accepts(active_block)
|| SeriesDetailsHandler::accepts(active_block)
|| LIBRARY_BLOCKS.contains(&active_block) || LIBRARY_BLOCKS.contains(&active_block)
} }
@@ -0,0 +1,610 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_text_box_keys;
use crate::handlers::sonarr_handlers::history::history_sorting_options;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::sonarr::sonarr_data::{
ActiveSonarrBlock, EDIT_SERIES_SELECTION_BLOCKS, SERIES_DETAILS_BLOCKS,
};
use crate::models::{BlockSelectionState, HorizontallyScrollableText, Scrollable};
use crate::network::sonarr_network::SonarrEvent;
#[cfg(test)]
#[path = "series_details_handler_tests.rs"]
mod series_details_handler_tests;
pub(super) struct SeriesDetailsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_sonarr_block: ActiveSonarrBlock,
_context: Option<ActiveSonarrBlock>,
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler<'a, 'b> {
fn accepts(active_block: ActiveSonarrBlock) -> bool {
SERIES_DETAILS_BLOCKS.contains(&active_block)
}
fn with(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveSonarrBlock,
_context: Option<ActiveSonarrBlock>,
) -> SeriesDetailsHandler<'a, 'b> {
SeriesDetailsHandler {
key,
app,
active_sonarr_block: active_block,
_context,
}
}
fn get_key(&self) -> Key {
self.key
}
fn is_ready(&self) -> bool {
if self.active_sonarr_block == ActiveSonarrBlock::SeriesHistory {
!self.app.is_loading && self.app.data.sonarr_data.series_history.is_some()
} else {
!self.app.is_loading
}
}
fn handle_scroll_up(&mut self) {
match self.active_sonarr_block {
ActiveSonarrBlock::SeriesDetails => self.app.data.sonarr_data.seasons.scroll_up(),
ActiveSonarrBlock::SeriesHistory => self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.scroll_up(),
ActiveSonarrBlock::SeriesHistorySortPrompt => self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.sort
.as_mut()
.unwrap()
.scroll_up(),
_ => (),
}
}
fn handle_scroll_down(&mut self) {
match self.active_sonarr_block {
ActiveSonarrBlock::SeriesDetails => self.app.data.sonarr_data.seasons.scroll_down(),
ActiveSonarrBlock::SeriesHistory => self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.scroll_down(),
ActiveSonarrBlock::SeriesHistorySortPrompt => self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.sort
.as_mut()
.unwrap()
.scroll_down(),
_ => (),
}
}
fn handle_home(&mut self) {
match self.active_sonarr_block {
ActiveSonarrBlock::SeriesDetails => self.app.data.sonarr_data.seasons.scroll_to_top(),
ActiveSonarrBlock::SearchSeason => self
.app
.data
.sonarr_data
.seasons
.search
.as_mut()
.unwrap()
.scroll_home(),
ActiveSonarrBlock::SearchSeriesHistory => self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.search
.as_mut()
.unwrap()
.scroll_home(),
ActiveSonarrBlock::FilterSeriesHistory => self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.filter
.as_mut()
.unwrap()
.scroll_home(),
ActiveSonarrBlock::SeriesHistory => self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.scroll_to_top(),
ActiveSonarrBlock::SeriesHistorySortPrompt => self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.sort
.as_mut()
.unwrap()
.scroll_to_top(),
_ => (),
}
}
fn handle_end(&mut self) {
match self.active_sonarr_block {
ActiveSonarrBlock::SeriesDetails => self.app.data.sonarr_data.seasons.scroll_to_bottom(),
ActiveSonarrBlock::SearchSeason => self
.app
.data
.sonarr_data
.seasons
.search
.as_mut()
.unwrap()
.reset_offset(),
ActiveSonarrBlock::SearchSeriesHistory => self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.search
.as_mut()
.unwrap()
.reset_offset(),
ActiveSonarrBlock::FilterSeriesHistory => self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.filter
.as_mut()
.unwrap()
.reset_offset(),
ActiveSonarrBlock::SeriesHistory => self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.scroll_to_bottom(),
ActiveSonarrBlock::SeriesHistorySortPrompt => self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.sort
.as_mut()
.unwrap()
.scroll_to_bottom(),
_ => (),
}
}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
match self.active_sonarr_block {
ActiveSonarrBlock::SeriesDetails | ActiveSonarrBlock::SeriesHistory => match self.key {
_ if self.key == DEFAULT_KEYBINDINGS.left.key => {
self.app.data.sonarr_data.series_info_tabs.previous();
self.app.pop_and_push_navigation_stack(
self
.app
.data
.sonarr_data
.series_info_tabs
.get_active_route(),
);
}
_ if self.key == DEFAULT_KEYBINDINGS.right.key => {
self.app.data.sonarr_data.series_info_tabs.next();
self.app.pop_and_push_navigation_stack(
self
.app
.data
.sonarr_data
.series_info_tabs
.get_active_route(),
);
}
_ => (),
},
ActiveSonarrBlock::UpdateAndScanSeriesPrompt
| ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => {
handle_prompt_toggle(self.app, self.key)
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_sonarr_block {
ActiveSonarrBlock::SeriesDetails if !self.app.data.sonarr_data.seasons.is_empty() => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
}
ActiveSonarrBlock::SeriesHistory
if !self
.app
.data
.sonarr_data
.series_history
.as_ref()
.expect("Series history should be Some")
.is_empty() =>
{
self
.app
.push_navigation_stack(ActiveSonarrBlock::SeriesHistoryDetails.into());
}
ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => {
if self.app.data.sonarr_data.prompt_confirm {
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::TriggerAutomaticSeriesSearch(None));
}
self.app.pop_navigation_stack();
}
ActiveSonarrBlock::UpdateAndScanSeriesPrompt => {
if self.app.data.sonarr_data.prompt_confirm {
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::UpdateAndScanSeries(None));
}
self.app.pop_navigation_stack();
}
ActiveSonarrBlock::SearchSeason => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
if self.app.data.sonarr_data.seasons.search.is_some() {
let has_match = self.app.data.sonarr_data.seasons.apply_search(|season| {
season
.title
.as_ref()
.expect("Season was not populated with title in handlers")
});
if !has_match {
self
.app
.push_navigation_stack(ActiveSonarrBlock::SearchSeasonError.into());
}
}
}
ActiveSonarrBlock::SearchSeriesHistory => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
if self
.app
.data
.sonarr_data
.series_history
.as_ref()
.unwrap()
.search
.is_some()
{
let has_match = self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.apply_search(|history_item| &history_item.source_title.text);
if !has_match {
self
.app
.push_navigation_stack(ActiveSonarrBlock::SearchSeriesHistoryError.into());
}
}
}
ActiveSonarrBlock::FilterSeriesHistory => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
if self
.app
.data
.sonarr_data
.series_history
.as_ref()
.unwrap()
.filter
.is_some()
{
let has_matches = self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.apply_filter(|history_item| &history_item.source_title.text);
if !has_matches {
self
.app
.push_navigation_stack(ActiveSonarrBlock::FilterSeriesHistoryError.into());
}
}
}
ActiveSonarrBlock::SeriesHistorySortPrompt => {
self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.items
.sort_by(|a, b| a.id.cmp(&b.id));
self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.apply_sorting();
self.app.pop_navigation_stack();
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_sonarr_block {
ActiveSonarrBlock::SearchSeason | ActiveSonarrBlock::SearchSeasonError => {
self.app.pop_navigation_stack();
self.app.data.sonarr_data.seasons.reset_search();
self.app.should_ignore_quit_key = false;
}
ActiveSonarrBlock::SearchSeriesHistory | ActiveSonarrBlock::SearchSeriesHistoryError => {
self.app.pop_navigation_stack();
self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.reset_search();
self.app.should_ignore_quit_key = false;
}
ActiveSonarrBlock::FilterSeriesHistory | ActiveSonarrBlock::FilterSeriesHistoryError => {
self.app.pop_navigation_stack();
self
.app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.reset_filter();
self.app.should_ignore_quit_key = false;
}
ActiveSonarrBlock::UpdateAndScanSeriesPrompt
| ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => {
self.app.pop_navigation_stack();
self.app.data.sonarr_data.prompt_confirm = false;
}
ActiveSonarrBlock::SeriesHistoryDetails | ActiveSonarrBlock::SeriesHistorySortPrompt => {
self.app.pop_navigation_stack();
}
ActiveSonarrBlock::SeriesHistory => {
if self.app.data.sonarr_data.series_history.as_ref().expect("Series history is not populated").filtered_items.is_some() {
self.app.data.sonarr_data.series_history.as_mut().expect("Series history is not populated").reset_filter();
} else {
self.app.pop_navigation_stack();
self.app.data.sonarr_data.reset_series_info_tabs();
}
}
ActiveSonarrBlock::SeriesDetails => {
self.app.pop_navigation_stack();
self.app.data.sonarr_data.reset_series_info_tabs();
}
_ => (),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_sonarr_block {
ActiveSonarrBlock::SeriesDetails => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => self
.app
.pop_and_push_navigation_stack(self.active_sonarr_block.into()),
_ if key == DEFAULT_KEYBINDINGS.search.key => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::SearchSeason.into());
self.app.data.sonarr_data.seasons.search = Some(HorizontallyScrollableText::default());
self.app.should_ignore_quit_key = true;
}
_ if key == DEFAULT_KEYBINDINGS.auto_search.key => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchSeriesPrompt.into());
}
_ if key == DEFAULT_KEYBINDINGS.update.key => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::UpdateAndScanSeriesPrompt.into());
}
_ if key == DEFAULT_KEYBINDINGS.edit.key => {
self.app.push_navigation_stack(
(
ActiveSonarrBlock::EditSeriesPrompt,
Some(self.active_sonarr_block),
)
.into(),
);
self.app.data.sonarr_data.edit_series_modal = Some((&self.app.data.sonarr_data).into());
self.app.data.sonarr_data.selected_block =
BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS);
}
_ => (),
},
ActiveSonarrBlock::SeriesHistory => match self.key {
_ if key == DEFAULT_KEYBINDINGS.refresh.key => self
.app
.pop_and_push_navigation_stack(self.active_sonarr_block.into()),
_ if key == DEFAULT_KEYBINDINGS.auto_search.key => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchSeriesPrompt.into());
}
_ if key == DEFAULT_KEYBINDINGS.search.key => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::SearchSeriesHistory.into());
self
.app
.data
.sonarr_data
.series_history
.as_mut()
.expect("Series history should be populated")
.search = Some(HorizontallyScrollableText::default());
self.app.should_ignore_quit_key = true;
}
_ if key == DEFAULT_KEYBINDINGS.filter.key => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::FilterSeriesHistory.into());
self
.app
.data
.sonarr_data
.series_history
.as_mut()
.expect("Series history should be populated")
.reset_filter();
self
.app
.data
.sonarr_data
.series_history
.as_mut()
.expect("Series history should be populated")
.filter = Some(HorizontallyScrollableText::default());
self.app.should_ignore_quit_key = true;
}
_ if key == DEFAULT_KEYBINDINGS.sort.key => {
self
.app
.data
.sonarr_data
.series_history
.as_mut()
.expect("Series history should be populated")
.sorting(history_sorting_options());
self
.app
.push_navigation_stack(ActiveSonarrBlock::SeriesHistorySortPrompt.into());
}
_ if key == DEFAULT_KEYBINDINGS.edit.key => {
self.app.push_navigation_stack(
(
ActiveSonarrBlock::EditSeriesPrompt,
Some(self.active_sonarr_block),
)
.into(),
);
self.app.data.sonarr_data.edit_series_modal = Some((&self.app.data.sonarr_data).into());
self.app.data.sonarr_data.selected_block =
BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS);
}
_ if key == DEFAULT_KEYBINDINGS.update.key => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::UpdateAndScanSeriesPrompt.into());
}
_ => (),
},
ActiveSonarrBlock::SearchSeason => {
handle_text_box_keys!(
self,
key,
self.app.data.sonarr_data.seasons.search.as_mut().unwrap()
)
}
ActiveSonarrBlock::SearchSeriesHistory => {
handle_text_box_keys!(
self,
key,
self.app.data.sonarr_data.series_history.as_mut().expect("Series history should be populated").search.as_mut().unwrap()
)
}
ActiveSonarrBlock::FilterSeriesHistory => {
handle_text_box_keys!(
self,
key,
self.app.data.sonarr_data.series_history.as_mut().expect("Series history should be populated").filter.as_mut().unwrap()
)
}
ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::TriggerAutomaticSeriesSearch(None));
self.app.pop_navigation_stack();
}
}
ActiveSonarrBlock::UpdateAndScanSeriesPrompt => {
if self.app.data.sonarr_data.prompt_confirm {
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::UpdateAndScanSeries(None));
}
self.app.pop_navigation_stack();
}
_ => (),
}
}
}
File diff suppressed because it is too large Load Diff
@@ -7,15 +7,15 @@ mod utils {
($handler:ident, $block:expr, $context:expr) => { ($handler:ident, $block:expr, $context:expr) => {
let mut app = App::default(); let mut app = App::default();
let mut sonarr_data = SonarrData { let mut sonarr_data = SonarrData {
quality_profile_map: BiMap::from_iter([ quality_profile_map: bimap::BiMap::from_iter([
(2222, "HD - 1080p".to_owned()), (2222, "HD - 1080p".to_owned()),
(1111, "Any".to_owned()), (1111, "Any".to_owned()),
]), ]),
language_profiles_map: BiMap::from_iter([ language_profiles_map: bimap::BiMap::from_iter([
(2222, "English".to_owned()), (2222, "English".to_owned()),
(1111, "Any".to_owned()), (1111, "Any".to_owned()),
]), ]),
tags_map: BiMap::from_iter([(1, "test".to_owned())]), tags_map: bimap::BiMap::from_iter([(1, "test".to_owned())]),
..create_test_sonarr_data() ..create_test_sonarr_data()
}; };
sonarr_data.series.set_items(vec![Series { sonarr_data.series.set_items(vec![Series {
+10 -2
View File
@@ -10,6 +10,7 @@ use crate::{
}, },
sonarr::sonarr_context_clues::{ sonarr::sonarr_context_clues::{
HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES,
SERIES_HISTORY_CONTEXT_CLUES,
}, },
}, },
models::{ models::{
@@ -80,6 +81,12 @@ impl<'a> SonarrData<'a> {
self.delete_series_files = false; self.delete_series_files = false;
self.add_list_exclusion = false; self.add_list_exclusion = false;
} }
pub fn reset_series_info_tabs(&mut self) {
self.series_history = None;
self.seasons = StatefulTable::default();
self.series_info_tabs.index = 0;
}
} }
impl<'a> Default for SonarrData<'a> { impl<'a> Default for SonarrData<'a> {
@@ -174,7 +181,7 @@ impl<'a> Default for SonarrData<'a> {
title: "History", title: "History",
route: ActiveSonarrBlock::SeriesHistory.into(), route: ActiveSonarrBlock::SeriesHistory.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES)),
}, },
]), ]),
} }
@@ -304,12 +311,13 @@ pub static LIBRARY_BLOCKS: [ActiveSonarrBlock; 7] = [
ActiveSonarrBlock::UpdateAllSeriesPrompt, ActiveSonarrBlock::UpdateAllSeriesPrompt,
]; ];
pub static SERIES_DETAILS_BLOCKS: [ActiveSonarrBlock; 11] = [ pub static SERIES_DETAILS_BLOCKS: [ActiveSonarrBlock; 12] = [
ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesDetails,
ActiveSonarrBlock::SeriesHistory, ActiveSonarrBlock::SeriesHistory,
ActiveSonarrBlock::SearchSeason, ActiveSonarrBlock::SearchSeason,
ActiveSonarrBlock::SearchSeasonError, ActiveSonarrBlock::SearchSeasonError,
ActiveSonarrBlock::UpdateAndScanSeriesPrompt, ActiveSonarrBlock::UpdateAndScanSeriesPrompt,
ActiveSonarrBlock::AutomaticallySearchSeriesPrompt,
ActiveSonarrBlock::SearchSeriesHistory, ActiveSonarrBlock::SearchSeriesHistory,
ActiveSonarrBlock::SearchSeriesHistoryError, ActiveSonarrBlock::SearchSeriesHistoryError,
ActiveSonarrBlock::FilterSeriesHistory, ActiveSonarrBlock::FilterSeriesHistory,
@@ -4,6 +4,9 @@ mod tests {
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use crate::app::sonarr::sonarr_context_clues::SERIES_HISTORY_CONTEXT_CLUES;
use crate::models::sonarr_models::{Season, SonarrHistoryItem};
use crate::models::stateful_table::StatefulTable;
use crate::{ use crate::{
app::{ app::{
context_clues::{ context_clues::{
@@ -56,6 +59,24 @@ mod tests {
assert!(!sonarr_data.add_list_exclusion); assert!(!sonarr_data.add_list_exclusion);
} }
#[test]
fn test_reset_series_info_tabs() {
let mut series_history = StatefulTable::default();
series_history.set_items(vec![SonarrHistoryItem::default()]);
let mut sonarr_data = SonarrData {
series_history: Some(series_history),
..SonarrData::default()
};
sonarr_data.seasons.set_items(vec![Season::default()]);
sonarr_data.series_info_tabs.index = 1;
sonarr_data.reset_series_info_tabs();
assert!(sonarr_data.series_history.is_none());
assert!(sonarr_data.seasons.is_empty());
assert_eq!(sonarr_data.series_info_tabs.index, 0);
}
#[test] #[test]
fn test_sonarr_data_defaults() { fn test_sonarr_data_defaults() {
let sonarr_data = SonarrData::default(); let sonarr_data = SonarrData::default();
@@ -195,7 +216,7 @@ mod tests {
assert!(sonarr_data.series_info_tabs.tabs[1].help.is_empty()); assert!(sonarr_data.series_info_tabs.tabs[1].help.is_empty());
assert_eq!( assert_eq!(
sonarr_data.series_info_tabs.tabs[1].contextual_help, sonarr_data.series_info_tabs.tabs[1].contextual_help,
Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)) Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES))
); );
} }
} }
@@ -570,12 +591,13 @@ mod tests {
#[test] #[test]
fn test_series_details_blocks_contents() { fn test_series_details_blocks_contents() {
assert_eq!(SERIES_DETAILS_BLOCKS.len(), 11); assert_eq!(SERIES_DETAILS_BLOCKS.len(), 12);
assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeriesDetails)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeriesDetails));
assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeriesHistory)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeriesHistory));
assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeason)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeason));
assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeasonError)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeasonError));
assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::UpdateAndScanSeriesPrompt)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::UpdateAndScanSeriesPrompt));
assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::AutomaticallySearchSeriesPrompt));
assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeriesHistory)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeriesHistory));
assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeriesHistoryError)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeriesHistoryError));
assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::FilterSeriesHistory)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::FilterSeriesHistory));
@@ -32,6 +32,8 @@ pub mod utils {
let mut seasons = StatefulTable::default(); let mut seasons = StatefulTable::default();
seasons.set_items(vec![Season::default()]); seasons.set_items(vec![Season::default()]);
let mut series_history = StatefulTable::default();
series_history.set_items(vec![SonarrHistoryItem::default()]);
let mut sonarr_data = SonarrData { let mut sonarr_data = SonarrData {
delete_series_files: true, delete_series_files: true,
@@ -39,6 +41,7 @@ pub mod utils {
add_series_search: Some("test search".into()), add_series_search: Some("test search".into()),
edit_root_folder: Some("test path".into()), edit_root_folder: Some("test path".into()),
seasons, seasons,
series_history: Some(series_history),
season_details_modal: Some(season_details_modal), season_details_modal: Some(season_details_modal),
add_searched_series: Some(StatefulTable::default()), add_searched_series: Some(StatefulTable::default()),
..SonarrData::default() ..SonarrData::default()
+1
View File
@@ -239,6 +239,7 @@ impl Eq for Rating {}
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] #[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Season { pub struct Season {
pub title: Option<String>,
#[serde(deserialize_with = "super::from_i64")] #[serde(deserialize_with = "super::from_i64")]
pub season_number: i64, pub season_number: i64,
pub monitored: bool, pub monitored: bool,
+3 -3
View File
@@ -3,7 +3,7 @@ mod test {
use std::sync::Arc; use std::sync::Arc;
use bimap::BiMap; use bimap::BiMap;
use chrono::{DateTime, Utc}; use chrono::DateTime;
use indoc::formatdoc; use indoc::formatdoc;
use mockito::{Matcher, Server}; use mockito::{Matcher, Server};
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
@@ -5237,8 +5237,7 @@ mod test {
) )
.await; .await;
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap()) let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap());
as DateTime<Utc>;
if let SonarrSerdeable::SystemStatus(status) = network if let SonarrSerdeable::SystemStatus(status) = network
.handle_sonarr_event(SonarrEvent::GetStatus) .handle_sonarr_event(SonarrEvent::GetStatus)
@@ -6874,6 +6873,7 @@ mod test {
fn season() -> Season { fn season() -> Season {
Season { Season {
title: None,
season_number: 1, season_number: 1,
monitored: true, monitored: true,
statistics: season_statistics(), statistics: season_statistics(),
@@ -60,7 +60,6 @@ impl DrawUi for MovieDetailsUi {
.prompt(&prompt) .prompt(&prompt)
.yes_no_value(app.data.radarr_data.prompt_confirm); .yes_no_value(app.data.radarr_data.prompt_confirm);
draw_movie_info(f, app, content_area);
f.render_widget( f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt), Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(), f.area(),
+1 -1
View File
@@ -82,7 +82,7 @@ impl DrawUi for EditSeriesUi {
} }
_ if SERIES_DETAILS_BLOCKS.contains(&context) => { _ if SERIES_DETAILS_BLOCKS.contains(&context) => {
draw_popup_over_ui::<SeriesDetailsUi>(f, app, area, draw_library, Size::Large); draw_popup_over_ui::<SeriesDetailsUi>(f, app, area, draw_library, Size::Large);
draw_popup(f, app, draw_edit_series_prompt, Size::Medium); draw_popup(f, app, draw_edit_series_prompt, Size::Long);
} }
_ => (), _ => (),
} }
+181 -33
View File
@@ -12,11 +12,11 @@ mod tests {
use crate::ui::DrawUi; use crate::ui::DrawUi;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use ratatui::widgets::{Cell, Row}; use ratatui::widgets::{Cell, Row};
use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::models::sonarr_models::{Season, SeasonStatistics};
use crate::{ use crate::{
models::sonarr_models::{Series, SeriesStatistics}, models::sonarr_models::Series,
ui::sonarr_ui::library::decorate_series_row_with_style, ui::sonarr_ui::library::decorate_series_row_with_style,
}; };
@@ -38,45 +38,193 @@ mod tests {
}); });
} }
#[rstest] #[test]
#[case(SeriesStatus::Ended, None, RowStyle::Missing)] fn test_decorate_row_with_style_downloaded_when_ended_and_all_monitored_episodes_are_present(
#[case(SeriesStatus::Ended, Some(59.0), RowStyle::Missing)]
#[case(SeriesStatus::Ended, Some(100.0), RowStyle::Downloaded)]
#[case(SeriesStatus::Continuing, None, RowStyle::Missing)]
#[case(SeriesStatus::Continuing, Some(59.0), RowStyle::Missing)]
#[case(SeriesStatus::Continuing, Some(100.0), RowStyle::Unreleased)]
#[case(SeriesStatus::Upcoming, None, RowStyle::Unreleased)]
#[case(SeriesStatus::Deleted, None, RowStyle::Missing)]
fn test_decorate_series_row_with_style(
#[case] series_status: SeriesStatus,
#[case] percent_of_episodes: Option<f64>,
#[case] expected_row_style: RowStyle,
) { ) {
let mut series = Series { let seasons = vec![
status: series_status, Season {
monitored: false,
statistics: SeasonStatistics {
episode_count: 1,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
Season {
monitored: true,
statistics: SeasonStatistics {
episode_count: 3,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
];
let series = Series {
status: SeriesStatus::Ended,
seasons: Some(seasons),
..Series::default() ..Series::default()
}; };
if let Some(percentage) = percent_of_episodes {
series.statistics = Some(SeriesStatistics {
percent_of_episodes: percentage,
..SeriesStatistics::default()
});
}
let row = Row::new(vec![Cell::from("test".to_owned())]); let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone()); let style = decorate_series_row_with_style(&series, row.clone());
match expected_row_style { assert_eq!(style, row.downloaded());
RowStyle::Downloaded => assert_eq!(style, row.downloaded()),
RowStyle::Missing => assert_eq!(style, row.missing()),
RowStyle::Unreleased => assert_eq!(style, row.unreleased()),
}
} }
enum RowStyle { #[test]
Downloaded, fn test_decorate_row_with_style_missing_when_ended_and_episodes_are_missing() {
Missing, let seasons = vec![
Unreleased, Season {
monitored: true,
statistics: SeasonStatistics {
episode_count: 1,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
Season {
monitored: true,
statistics: SeasonStatistics {
episode_count: 3,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
];
let series = Series {
status: SeriesStatus::Ended,
seasons: Some(seasons),
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.missing());
}
#[test]
fn test_decorate_row_with_style_indeterminate_when_ended_and_seasons_is_empty() {
let series = Series {
status: SeriesStatus::Ended,
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.indeterminate());
}
#[test]
fn test_decorate_row_with_style_unreleased_when_continuing_and_all_monitored_episodes_are_present(
) {
let seasons = vec![
Season {
monitored: false,
statistics: SeasonStatistics {
episode_count: 1,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
Season {
monitored: true,
statistics: SeasonStatistics {
episode_count: 3,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
];
let series = Series {
status: SeriesStatus::Continuing,
seasons: Some(seasons),
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.unreleased());
}
#[test]
fn test_decorate_row_with_style_missing_when_continuing_and_episodes_are_missing() {
let seasons = vec![
Season {
monitored: true,
statistics: SeasonStatistics {
episode_count: 1,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
Season {
monitored: true,
statistics: SeasonStatistics {
episode_count: 3,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
];
let series = Series {
status: SeriesStatus::Continuing,
seasons: Some(seasons),
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.missing());
}
#[test]
fn test_decorate_row_with_style_indeterminate_when_continuing_and_seasons_is_empty() {
let series = Series {
status: SeriesStatus::Continuing,
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.indeterminate());
}
#[test]
fn test_decorate_row_with_style_unreleased_when_upcoming() {
let series = Series {
status: SeriesStatus::Upcoming,
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.unreleased());
}
#[test]
fn test_decorate_row_with_style_defaults_to_indeterminate() {
let series = Series {
status: SeriesStatus::Deleted,
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.indeterminate());
} }
} }
+24 -41
View File
@@ -61,35 +61,6 @@ impl DrawUi for LibraryUi {
| ActiveSonarrBlock::SearchSeriesError | ActiveSonarrBlock::SearchSeriesError
| ActiveSonarrBlock::FilterSeries | ActiveSonarrBlock::FilterSeries
| ActiveSonarrBlock::FilterSeriesError => draw_library(f, app, area), | ActiveSonarrBlock::FilterSeriesError => draw_library(f, app, area),
// ActiveSonarrBlock::SearchSeries => draw_popup_over(
// f,
// app,
// area,
// draw_library,
// draw_library_search_box,
// Size::InputBox,
// ),
// ActiveSonarrBlock::SearchSeriesError => {
// let popup = Popup::new(Message::new("Series not found!")).size(Size::Message);
// draw_library(f, app, area);
// f.render_widget(popup, f.area());
// }
// ActiveSonarrBlock::FilterSeries => draw_popup_over(
// f,
// app,
// area,
// draw_library,
// draw_filter_series_box,
// Size::InputBox,
// ),
// ActiveSonarrBlock::FilterSeriesError => {
// let popup = Popup::new(Message::new("No series found matching the given filter!"))
// .size(Size::Message);
// draw_library(f, app, area);
// f.render_widget(popup, f.area());
// }
ActiveSonarrBlock::UpdateAllSeriesPrompt => { ActiveSonarrBlock::UpdateAllSeriesPrompt => {
let confirmation_prompt = ConfirmationPrompt::new() let confirmation_prompt = ConfirmationPrompt::new()
.title("Update All Series") .title("Update All Series")
@@ -234,24 +205,36 @@ pub(super) fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
fn decorate_series_row_with_style<'a>(series: &Series, row: Row<'a>) -> Row<'a> { fn decorate_series_row_with_style<'a>(series: &Series, row: Row<'a>) -> Row<'a> {
match series.status { match series.status {
SeriesStatus::Ended => { SeriesStatus::Ended => {
if let Some(ref stats) = series.statistics { if let Some(ref seasons) = series.seasons {
if stats.percent_of_episodes == 100.0 { return if seasons
return row.downloaded(); .iter()
.filter(|season| season.monitored)
.all(|season| season.statistics.episode_count == season.statistics.total_episode_count)
{
row.downloaded()
} else {
row.missing()
} }
} }
row.missing() row.indeterminate()
} }
SeriesStatus::Continuing => { SeriesStatus::Continuing => {
if let Some(ref stats) = series.statistics { if let Some(ref seasons) = series.seasons {
if stats.percent_of_episodes == 100.0 { return if seasons
return row.unreleased(); .iter()
} .filter(|season| season.monitored)
.all(|season| season.statistics.episode_count == season.statistics.total_episode_count)
{
row.unreleased()
} else {
row.missing()
};
} }
row.missing() row.indeterminate()
} }
SeriesStatus::Upcoming => row.unreleased(), SeriesStatus::Upcoming => row.unreleased(),
_ => row.missing(), _ => row.indeterminate(),
} }
} }
+35 -19
View File
@@ -1,8 +1,10 @@
use deunicode::deunicode;
use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Style, Stylize}; use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Text}; use ratatui::text::{Line, Text};
use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
use ratatui::Frame; use ratatui::Frame;
use regex::Regex;
use crate::app::App; use crate::app::App;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SERIES_DETAILS_BLOCKS}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SERIES_DETAILS_BLOCKS};
@@ -67,6 +69,20 @@ impl DrawUi for SeriesDetailsUi {
draw_series_details(f, app, content_area); draw_series_details(f, app, content_area);
match active_sonarr_block { match active_sonarr_block {
ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => {
let prompt = format!(
"Do you want to trigger an automatic search of your indexers for all monitored episode(s) for the series: {}", app.data.sonarr_data.series.current_selection().title
);
let confirmation_prompt = ConfirmationPrompt::new()
.title("Automatic Series Search")
.prompt(&prompt)
.yes_no_value(app.data.sonarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
ActiveSonarrBlock::UpdateAndScanSeriesPrompt => { ActiveSonarrBlock::UpdateAndScanSeriesPrompt => {
let prompt = format!( let prompt = format!(
"Do you want to trigger an update and disk scan for the series: {}?", "Do you want to trigger an update and disk scan for the series: {}?",
@@ -83,14 +99,7 @@ impl DrawUi for SeriesDetailsUi {
); );
} }
ActiveSonarrBlock::SeriesHistoryDetails => { ActiveSonarrBlock::SeriesHistoryDetails => {
draw_popup_over( draw_history_item_details_popup(f, app, popup_area);
f,
app,
popup_area,
draw_series_history_table,
draw_history_item_details_popup,
Size::Small,
);
} }
_ => (), _ => (),
}; };
@@ -129,19 +138,25 @@ pub fn draw_series_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
.get_by_left(&current_selection.language_profile_id) .get_by_left(&current_selection.language_profile_id)
.unwrap() .unwrap()
.to_owned(); .to_owned();
let overview = Regex::new(r"[\r\n\t]")
.unwrap()
.replace_all(
&deunicode(
current_selection
.overview
.as_ref()
.unwrap_or(&String::new()),
),
"",
)
.to_string();
let mut series_description = vec![ let mut series_description = vec![
Line::from(vec![ Line::from(vec![
"Title: ".primary().bold(), "Title: ".primary().bold(),
current_selection.title.text.clone().primary().bold(), current_selection.title.text.clone().primary().bold(),
]), ]),
Line::from(vec![ Line::from(vec!["Overview: ".primary().bold(), overview.default()]),
"Overview: ".primary().bold(),
current_selection
.overview
.clone()
.unwrap_or_default()
.default(),
]),
Line::from(vec![ Line::from(vec![
"Network: ".primary().bold(), "Network: ".primary().bold(),
current_selection current_selection
@@ -194,7 +209,7 @@ pub fn draw_series_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
let description_paragraph = Paragraph::new(series_description) let description_paragraph = Paragraph::new(series_description)
.block(borderless_block()) .block(borderless_block())
.wrap(Wrap { trim: false }); .wrap(Wrap { trim: true });
f.render_widget(description_paragraph, area); f.render_widget(description_paragraph, area);
} }
@@ -220,9 +235,10 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.get_active_tab_contextual_help(); .get_active_tab_contextual_help();
let season_row_mapping = |season: &Season| { let season_row_mapping = |season: &Season| {
let Season { let Season {
season_number, title,
monitored, monitored,
statistics, statistics,
..
} = season; } = season;
let SeasonStatistics { let SeasonStatistics {
episode_count, episode_count,
@@ -235,7 +251,7 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let row = Row::new(vec![ let row = Row::new(vec![
Cell::from(season_monitored.to_owned()), Cell::from(season_monitored.to_owned()),
Cell::from(format!("Season {}", season_number)), Cell::from(title.clone().unwrap()),
Cell::from(format!("{}/{}", episode_count, total_episode_count)), Cell::from(format!("{}/{}", episode_count, total_episode_count)),
Cell::from(format!("{size:.2} GB")), Cell::from(format!("{size:.2} GB")),
]); ]);
+5
View File
@@ -14,6 +14,7 @@ where
#[allow(clippy::new_ret_no_self)] #[allow(clippy::new_ret_no_self)]
fn new() -> T; fn new() -> T;
fn awaiting_import(self) -> T; fn awaiting_import(self) -> T;
fn indeterminate(self) -> T;
fn default(self) -> T; fn default(self) -> T;
fn downloaded(self) -> T; fn downloaded(self) -> T;
fn downloading(self) -> T; fn downloading(self) -> T;
@@ -44,6 +45,10 @@ where
self.fg(COLOR_ORANGE) self.fg(COLOR_ORANGE)
} }
fn indeterminate(self) -> T {
self.fg(COLOR_ORANGE)
}
fn default(self) -> T { fn default(self) -> T {
self.white() self.white()
} }
+8
View File
@@ -18,6 +18,14 @@ mod test {
); );
} }
#[test]
fn test_style_indeterminate() {
assert_eq!(
Style::new().indeterminate(),
Style::new().fg(COLOR_ORANGE)
);
}
#[test] #[test]
fn test_style_default() { fn test_style_default() {
assert_eq!(Style::new().default(), Style::new().white()); assert_eq!(Style::new().default(), Style::new().white());
+6 -30
View File
@@ -1,50 +1,26 @@
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{layout_block, style_block_highlight}; use crate::ui::utils::{layout_block, style_block_highlight};
use derive_setters::Setters;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::prelude::{Style, Text, Widget}; use ratatui::prelude::{Style, Text, Widget};
use ratatui::style::Styled; use ratatui::style::Styled;
use ratatui::widgets::Paragraph; use ratatui::widgets::Paragraph;
#[cfg(test)] #[derive(Default, Setters)]
#[path = "button_tests.rs"]
mod button_tests;
#[derive(Default)]
pub struct Button<'a> { pub struct Button<'a> {
title: &'a str, title: &'a str,
#[setters(strip_option)]
label: Option<&'a str>, label: Option<&'a str>,
#[setters(strip_option)]
icon: Option<&'a str>, icon: Option<&'a str>,
#[setters(into)]
style: Style, style: Style,
#[setters(rename = "selected")]
is_selected: bool, is_selected: bool,
} }
impl<'a> Button<'a> { impl<'a> Button<'a> {
pub fn title(mut self, title: &'a str) -> Button<'a> {
self.title = title;
self
}
pub fn label(mut self, label: &'a str) -> Button<'a> {
self.label = Some(label);
self
}
pub fn icon(mut self, icon: &'a str) -> Button<'a> {
self.icon = Some(icon);
self
}
pub fn style<S: Into<Style>>(mut self, style: S) -> Button<'a> {
self.style = style.into();
self
}
pub fn selected(mut self, is_selected: bool) -> Button<'a> {
self.is_selected = is_selected;
self
}
fn render_button_with_icon(self, area: Rect, buf: &mut Buffer) { fn render_button_with_icon(self, area: Rect, buf: &mut Buffer) {
let [title_area, icon_area] = Layout::horizontal([ let [title_area, icon_area] = Layout::horizontal([
Constraint::Length(self.title.len() as u16), Constraint::Length(self.title.len() as u16),
-41
View File
@@ -1,41 +0,0 @@
#[cfg(test)]
mod tests {
use crate::ui::widgets::button::Button;
use pretty_assertions::{assert_eq, assert_str_eq};
use ratatui::style::{Style, Stylize};
#[test]
fn test_title() {
let button = Button::default().title("Title");
assert_str_eq!(button.title, "Title");
}
#[test]
fn test_label() {
let button = Button::default().label("Label");
assert_eq!(button.label, Some("Label"));
}
#[test]
fn test_icon() {
let button = Button::default().icon("Icon");
assert_eq!(button.icon, Some("Icon"));
}
#[test]
fn test_style() {
let button = Button::default().style(Style::new().bold());
assert_eq!(button.style, Style::new().bold());
}
#[test]
fn test_selected() {
let button = Button::default().selected(true);
assert!(button.is_selected);
}
}
+5 -15
View File
@@ -1,3 +1,4 @@
use derive_setters::Setters;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{borderless_block, layout_block, style_block_highlight}; use crate::ui::utils::{borderless_block, layout_block, style_block_highlight};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
@@ -6,14 +7,13 @@ use ratatui::prelude::Text;
use ratatui::style::Stylize; use ratatui::style::Stylize;
use ratatui::widgets::{Paragraph, Widget}; use ratatui::widgets::{Paragraph, Widget};
#[cfg(test)] #[derive(PartialEq, Debug, Copy, Clone, Setters)]
#[path = "checkbox_tests.rs"]
mod checkbox_tests;
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Checkbox<'a> { pub struct Checkbox<'a> {
#[setters(skip)]
label: &'a str, label: &'a str,
#[setters(rename = "checked")]
is_checked: bool, is_checked: bool,
#[setters(rename = "highlighted")]
is_highlighted: bool, is_highlighted: bool,
} }
@@ -26,16 +26,6 @@ impl<'a> Checkbox<'a> {
} }
} }
pub fn checked(mut self, is_checked: bool) -> Checkbox<'a> {
self.is_checked = is_checked;
self
}
pub fn highlighted(mut self, is_selected: bool) -> Checkbox<'a> {
self.is_highlighted = is_selected;
self
}
fn render_checkbox(self, area: Rect, buf: &mut Buffer) { fn render_checkbox(self, area: Rect, buf: &mut Buffer) {
let check = if self.is_checked { "" } else { "" }; let check = if self.is_checked { "" } else { "" };
let [label_area, checkbox_area] = let [label_area, checkbox_area] =
-32
View File
@@ -1,32 +0,0 @@
#[cfg(test)]
mod tests {
use crate::ui::widgets::checkbox::Checkbox;
use pretty_assertions::assert_str_eq;
#[test]
fn test_checkbox_new() {
let checkbox = Checkbox::new("test");
assert_str_eq!(checkbox.label, "test");
assert!(!checkbox.is_checked);
assert!(!checkbox.is_highlighted);
}
#[test]
fn test_checkbox_checked() {
let checkbox = Checkbox::new("test").checked(true);
assert_str_eq!(checkbox.label, "test");
assert!(checkbox.is_checked);
assert!(!checkbox.is_highlighted);
}
#[test]
fn test_checkbox_highlighted() {
let checkbox = Checkbox::new("test").highlighted(true);
assert_str_eq!(checkbox.label, "test");
assert!(!checkbox.is_checked);
assert!(checkbox.is_highlighted);
}
}
+4 -30
View File
@@ -8,15 +8,19 @@ use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::text::Text; use ratatui::text::Text;
use ratatui::widgets::{Paragraph, Widget}; use ratatui::widgets::{Paragraph, Widget};
use std::iter; use std::iter;
use derive_setters::Setters;
#[cfg(test)] #[cfg(test)]
#[path = "confirmation_prompt_tests.rs"] #[path = "confirmation_prompt_tests.rs"]
mod confirmation_prompt_tests; mod confirmation_prompt_tests;
#[derive(Setters)]
pub struct ConfirmationPrompt<'a> { pub struct ConfirmationPrompt<'a> {
title: &'a str, title: &'a str,
prompt: &'a str, prompt: &'a str,
#[setters(strip_option)]
content: Option<Paragraph<'a>>, content: Option<Paragraph<'a>>,
#[setters(strip_option)]
checkboxes: Option<Vec<Checkbox<'a>>>, checkboxes: Option<Vec<Checkbox<'a>>>,
yes_no_value: bool, yes_no_value: bool,
yes_no_highlighted: bool, yes_no_highlighted: bool,
@@ -34,36 +38,6 @@ impl<'a> ConfirmationPrompt<'a> {
} }
} }
pub fn title(mut self, title: &'a str) -> Self {
self.title = title;
self
}
pub fn prompt(mut self, prompt: &'a str) -> Self {
self.prompt = prompt;
self
}
pub fn content(mut self, content: Paragraph<'a>) -> Self {
self.content = Some(content);
self
}
pub fn checkboxes(mut self, checkboxes: Vec<Checkbox<'a>>) -> Self {
self.checkboxes = Some(checkboxes);
self
}
pub fn yes_no_value(mut self, yes_highlighted: bool) -> Self {
self.yes_no_value = yes_highlighted;
self
}
pub fn yes_no_highlighted(mut self, yes_highlighted: bool) -> Self {
self.yes_no_highlighted = yes_highlighted;
self
}
fn render_confirmation_prompt_with_checkboxes(self, area: Rect, buf: &mut Buffer) { fn render_confirmation_prompt_with_checkboxes(self, area: Rect, buf: &mut Buffer) {
title_block_centered(self.title).render(area, buf); title_block_centered(self.title).render(area, buf);
let help_text = let help_text =
@@ -1,9 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::ui::widgets::checkbox::Checkbox;
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use ratatui::widgets::Paragraph;
#[test] #[test]
fn test_confirmation_prompt_new() { fn test_confirmation_prompt_new() {
@@ -16,78 +14,4 @@ mod tests {
assert!(!confirmation_prompt.yes_no_value); assert!(!confirmation_prompt.yes_no_value);
assert!(confirmation_prompt.yes_no_highlighted); assert!(confirmation_prompt.yes_no_highlighted);
} }
#[test]
fn test_confirmation_prompt_title() {
let confirmation_prompt = ConfirmationPrompt::new().title("title");
assert_str_eq!(confirmation_prompt.title, "title");
assert_str_eq!(confirmation_prompt.prompt, "");
assert_eq!(confirmation_prompt.content, None);
assert_eq!(confirmation_prompt.checkboxes, None);
assert!(!confirmation_prompt.yes_no_value);
assert!(confirmation_prompt.yes_no_highlighted);
}
#[test]
fn test_confirmation_prompt_prompt() {
let confirmation_prompt = ConfirmationPrompt::new().prompt("prompt");
assert_str_eq!(confirmation_prompt.prompt, "prompt");
assert_str_eq!(confirmation_prompt.title, "");
assert_eq!(confirmation_prompt.content, None);
assert_eq!(confirmation_prompt.checkboxes, None);
assert!(!confirmation_prompt.yes_no_value);
assert!(confirmation_prompt.yes_no_highlighted);
}
#[test]
fn test_confirmation_prompt_content() {
let content = Paragraph::new("content");
let confirmation_prompt = ConfirmationPrompt::new().content(content.clone());
assert_eq!(confirmation_prompt.content, Some(content));
assert_str_eq!(confirmation_prompt.title, "");
assert_str_eq!(confirmation_prompt.prompt, "");
assert_eq!(confirmation_prompt.checkboxes, None);
assert!(!confirmation_prompt.yes_no_value);
assert!(confirmation_prompt.yes_no_highlighted);
}
#[test]
fn test_confirmation_prompt_checkboxes() {
let checkboxes = vec![Checkbox::new("test").highlighted(true).checked(false)];
let confirmation_prompt = ConfirmationPrompt::new().checkboxes(checkboxes.clone());
assert_eq!(confirmation_prompt.checkboxes, Some(checkboxes));
assert_str_eq!(confirmation_prompt.title, "");
assert_str_eq!(confirmation_prompt.prompt, "");
assert_eq!(confirmation_prompt.content, None);
assert!(!confirmation_prompt.yes_no_value);
assert!(confirmation_prompt.yes_no_highlighted);
}
#[test]
fn test_confirmation_prompt_yes_no_value() {
let confirmation_prompt = ConfirmationPrompt::new().yes_no_value(true);
assert!(confirmation_prompt.yes_no_value);
assert_str_eq!(confirmation_prompt.title, "");
assert_str_eq!(confirmation_prompt.prompt, "");
assert_eq!(confirmation_prompt.content, None);
assert_eq!(confirmation_prompt.checkboxes, None);
assert!(confirmation_prompt.yes_no_highlighted);
}
#[test]
fn test_confirmation_prompt_yes_no_highlighted() {
let confirmation_prompt = ConfirmationPrompt::new().yes_no_highlighted(false);
assert!(!confirmation_prompt.yes_no_highlighted);
assert_str_eq!(confirmation_prompt.title, "");
assert_str_eq!(confirmation_prompt.prompt, "");
assert_eq!(confirmation_prompt.content, None);
assert_eq!(confirmation_prompt.checkboxes, None);
assert!(!confirmation_prompt.yes_no_value);
}
} }
+6 -36
View File
@@ -1,3 +1,4 @@
use derive_setters::Setters;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Layout, Position, Rect}; use ratatui::layout::{Constraint, Layout, Position, Rect};
use ratatui::prelude::Text; use ratatui::prelude::Text;
@@ -12,16 +13,20 @@ use crate::ui::utils::{borderless_block, layout_block};
#[path = "input_box_tests.rs"] #[path = "input_box_tests.rs"]
mod input_box_tests; mod input_box_tests;
#[derive(Default)] #[derive(Default, Setters)]
#[cfg_attr(test, derive(Debug, PartialEq))] #[cfg_attr(test, derive(Debug, PartialEq))]
pub struct InputBox<'a> { pub struct InputBox<'a> {
content: &'a str, content: &'a str,
offset: usize, offset: usize,
#[setters(into)]
style: Style, style: Style,
block: Block<'a>, block: Block<'a>,
#[setters(strip_option)]
label: Option<&'a str>, label: Option<&'a str>,
cursor_after_string: bool, cursor_after_string: bool,
#[setters(rename = "highlighted", strip_option)]
is_highlighted: Option<bool>, is_highlighted: Option<bool>,
#[setters(rename = "selected", strip_option)]
is_selected: Option<bool>, is_selected: Option<bool>,
} }
@@ -39,41 +44,6 @@ impl<'a> InputBox<'a> {
} }
} }
pub fn style<S: Into<Style>>(mut self, style: S) -> InputBox<'a> {
self.style = style.into();
self
}
pub fn block(mut self, block: Block<'a>) -> InputBox<'a> {
self.block = block;
self
}
pub fn label(mut self, label: &'a str) -> InputBox<'a> {
self.label = Some(label);
self
}
pub fn offset(mut self, offset: usize) -> InputBox<'a> {
self.offset = offset;
self
}
pub fn cursor_after_string(mut self, cursor_after_string: bool) -> InputBox<'a> {
self.cursor_after_string = cursor_after_string;
self
}
pub fn highlighted(mut self, is_highlighted: bool) -> InputBox<'a> {
self.is_highlighted = Some(is_highlighted);
self
}
pub fn selected(mut self, is_selected: bool) -> InputBox<'a> {
self.is_selected = Some(is_selected);
self
}
pub fn is_selected(&self) -> bool { pub fn is_selected(&self) -> bool {
self.is_selected.unwrap_or_default() self.is_selected.unwrap_or_default()
} }
-98
View File
@@ -20,104 +20,6 @@ mod tests {
assert_eq!(input_box.is_selected, None); assert_eq!(input_box.is_selected, None);
} }
#[test]
fn test_input_box_style() {
let input_box = InputBox::new("test").style(Style::new().highlight());
assert_eq!(input_box.style, Style::new().highlight());
assert_str_eq!(input_box.content, "test");
assert_eq!(input_box.offset, 0);
assert_eq!(input_box.block, layout_block());
assert_eq!(input_box.label, None);
assert!(input_box.cursor_after_string);
assert_eq!(input_box.is_highlighted, None);
assert_eq!(input_box.is_selected, None);
}
#[test]
fn test_input_box_block() {
let input_box = InputBox::new("test").block(layout_block().title("title"));
assert_eq!(input_box.block, layout_block().title("title"));
assert_str_eq!(input_box.content, "test");
assert_eq!(input_box.offset, 0);
assert_eq!(input_box.style, Style::new().default());
assert_eq!(input_box.label, None);
assert!(input_box.cursor_after_string);
assert_eq!(input_box.is_highlighted, None);
assert_eq!(input_box.is_selected, None);
}
#[test]
fn test_input_box_label() {
let input_box = InputBox::new("test").label("label");
assert_str_eq!(input_box.label.unwrap(), "label");
assert_str_eq!(input_box.content, "test");
assert_eq!(input_box.offset, 0);
assert_eq!(input_box.style, Style::new().default());
assert_eq!(input_box.block, layout_block());
assert!(input_box.cursor_after_string);
assert_eq!(input_box.is_highlighted, None);
assert_eq!(input_box.is_selected, None);
}
#[test]
fn test_input_box_offset() {
let input_box = InputBox::new("test").offset(1);
assert_eq!(input_box.offset, 1);
assert_str_eq!(input_box.content, "test");
assert_eq!(input_box.style, Style::new().default());
assert_eq!(input_box.block, layout_block());
assert_eq!(input_box.label, None);
assert!(input_box.cursor_after_string);
assert_eq!(input_box.is_highlighted, None);
assert_eq!(input_box.is_selected, None);
}
#[test]
fn test_input_box_cursor_after_string() {
let input_box = InputBox::new("test").cursor_after_string(false);
assert!(!input_box.cursor_after_string);
assert_str_eq!(input_box.content, "test");
assert_eq!(input_box.offset, 0);
assert_eq!(input_box.style, Style::new().default());
assert_eq!(input_box.block, layout_block());
assert_eq!(input_box.label, None);
assert_eq!(input_box.is_highlighted, None);
assert_eq!(input_box.is_selected, None);
}
#[test]
fn test_input_box_highlighted() {
let input_box = InputBox::new("test").highlighted(true);
assert_eq!(input_box.is_highlighted, Some(true));
assert_str_eq!(input_box.content, "test");
assert_eq!(input_box.offset, 0);
assert_eq!(input_box.style, Style::new().default());
assert_eq!(input_box.block, layout_block());
assert_eq!(input_box.label, None);
assert!(input_box.cursor_after_string);
assert_eq!(input_box.is_selected, None);
}
#[test]
fn test_input_box_selected() {
let input_box = InputBox::new("test").selected(true);
assert_eq!(input_box.is_selected, Some(true));
assert_str_eq!(input_box.content, "test");
assert_eq!(input_box.offset, 0);
assert_eq!(input_box.style, Style::new().default());
assert_eq!(input_box.block, layout_block());
assert_eq!(input_box.label, None);
assert!(input_box.cursor_after_string);
assert_eq!(input_box.is_highlighted, None);
}
#[test] #[test]
fn test_input_box_is_selected() { fn test_input_box_is_selected() {
let input_box = InputBox::new("test").selected(true); let input_box = InputBox::new("test").selected(true);
+9 -56
View File
@@ -12,7 +12,7 @@ use ratatui::widgets::{Block, ListItem, Paragraph, Row, StatefulWidget, Table, W
use ratatui::Frame; use ratatui::Frame;
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use derive_setters::Setters;
use super::input_box_popup::InputBoxPopup; use super::input_box_popup::InputBoxPopup;
use super::message::Message; use super::message::Message;
use super::popup::Size; use super::popup::Size;
@@ -21,24 +21,32 @@ use super::popup::Size;
#[path = "managarr_table_tests.rs"] #[path = "managarr_table_tests.rs"]
mod managarr_table_tests; mod managarr_table_tests;
#[derive(Setters)]
pub struct ManagarrTable<'a, T, F> pub struct ManagarrTable<'a, T, F>
where where
F: Fn(&T) -> Row<'a>, F: Fn(&T) -> Row<'a>,
T: Clone + PartialEq + Eq + Debug, T: Clone + PartialEq + Eq + Debug,
{ {
#[setters(strip_option)]
content: Option<&'a mut StatefulTable<T>>, content: Option<&'a mut StatefulTable<T>>,
#[setters(skip)]
table_headers: Vec<String>, table_headers: Vec<String>,
#[setters(skip)]
constraints: Vec<Constraint>, constraints: Vec<Constraint>,
row_mapper: F, row_mapper: F,
footer: Option<String>, footer: Option<String>,
footer_alignment: Alignment, footer_alignment: Alignment,
block: Block<'a>, block: Block<'a>,
margin: u16, margin: u16,
#[setters(rename = "loading")]
is_loading: bool, is_loading: bool,
highlight_rows: bool, highlight_rows: bool,
#[setters(rename = "sorting")]
is_sorting: bool, is_sorting: bool,
#[setters(rename = "searching")]
is_searching: bool, is_searching: bool,
search_produced_empty_results: bool, search_produced_empty_results: bool,
#[setters(rename = "filtering")]
is_filtering: bool, is_filtering: bool,
filter_produced_empty_results: bool, filter_produced_empty_results: bool,
search_box_content_length: usize, search_box_content_length: usize,
@@ -107,61 +115,6 @@ where
self self
} }
pub fn footer(mut self, footer: Option<String>) -> Self {
self.footer = footer;
self
}
pub fn footer_alignment(mut self, alignment: Alignment) -> Self {
self.footer_alignment = alignment;
self
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = block;
self
}
pub fn margin(mut self, margin: u16) -> Self {
self.margin = margin;
self
}
pub fn loading(mut self, is_loading: bool) -> Self {
self.is_loading = is_loading;
self
}
pub fn highlight_rows(mut self, highlight_rows: bool) -> Self {
self.highlight_rows = highlight_rows;
self
}
pub fn sorting(mut self, is_sorting: bool) -> Self {
self.is_sorting = is_sorting;
self
}
pub fn searching(mut self, is_searching: bool) -> Self {
self.is_searching = is_searching;
self
}
pub fn search_produced_empty_results(mut self, no_search_results: bool) -> Self {
self.search_produced_empty_results = no_search_results;
self
}
pub fn filtering(mut self, is_filtering: bool) -> Self {
self.is_filtering = is_filtering;
self
}
pub fn filter_produced_empty_results(mut self, no_filter_results: bool) -> Self {
self.filter_produced_empty_results = no_filter_results;
self
}
fn render_table(self, area: Rect, buf: &mut Buffer) { fn render_table(self, area: Rect, buf: &mut Buffer) {
let table_headers = self.parse_headers(); let table_headers = self.parse_headers();
let table_area = if let Some(ref footer) = self.footer { let table_area = if let Some(ref footer) = self.footer {
-353
View File
@@ -3,7 +3,6 @@ mod tests {
use crate::models::stateful_list::StatefulList; use crate::models::stateful_list::StatefulList;
use crate::models::stateful_table::{SortOption, StatefulTable}; use crate::models::stateful_table::{SortOption, StatefulTable};
use crate::models::{HorizontallyScrollableText, Scrollable}; use crate::models::{HorizontallyScrollableText, Scrollable};
use crate::ui::utils::layout_block;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use ratatui::layout::{Alignment, Constraint}; use ratatui::layout::{Alignment, Constraint};
@@ -180,358 +179,6 @@ mod tests {
assert_eq!(managarr_table.filter_box_offset, 0); assert_eq!(managarr_table.filter_box_offset, 0);
} }
#[test]
fn test_managarr_table_footer() {
let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(items.clone());
let footer = "footer".to_owned();
let managarr_table =
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)]))
.footer(Some(footer.clone()));
let row_mapper = managarr_table.row_mapper;
assert_eq!(managarr_table.footer, Some(footer));
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
assert_eq!(managarr_table.block, Block::new());
assert_eq!(managarr_table.margin, 0);
assert!(!managarr_table.is_loading);
assert!(managarr_table.highlight_rows);
assert!(!managarr_table.is_sorting);
assert!(!managarr_table.is_searching);
assert!(!managarr_table.search_produced_empty_results);
assert!(!managarr_table.is_filtering);
assert!(!managarr_table.filter_produced_empty_results);
assert_eq!(managarr_table.search_box_content_length, 0);
assert_eq!(managarr_table.search_box_offset, 0);
assert_eq!(managarr_table.filter_box_content_length, 0);
assert_eq!(managarr_table.filter_box_offset, 0);
}
#[test]
fn test_managarr_table_footer_alignment() {
let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(items.clone());
let managarr_table =
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)]))
.footer_alignment(Alignment::Center);
let row_mapper = managarr_table.row_mapper;
assert_eq!(managarr_table.footer_alignment, Alignment::Center);
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.footer, None);
assert_eq!(managarr_table.block, Block::new());
assert_eq!(managarr_table.margin, 0);
assert!(!managarr_table.is_loading);
assert!(managarr_table.highlight_rows);
assert!(!managarr_table.is_sorting);
assert!(!managarr_table.is_searching);
assert!(!managarr_table.search_produced_empty_results);
assert!(!managarr_table.is_filtering);
assert!(!managarr_table.filter_produced_empty_results);
assert_eq!(managarr_table.search_box_content_length, 0);
assert_eq!(managarr_table.search_box_offset, 0);
assert_eq!(managarr_table.filter_box_content_length, 0);
assert_eq!(managarr_table.filter_box_offset, 0);
}
#[test]
fn test_managarr_table_block() {
let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(items.clone());
let managarr_table =
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)]))
.block(layout_block());
let row_mapper = managarr_table.row_mapper;
assert_eq!(managarr_table.block, layout_block());
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.footer, None);
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
assert_eq!(managarr_table.margin, 0);
assert!(!managarr_table.is_loading);
assert!(managarr_table.highlight_rows);
assert!(!managarr_table.is_sorting);
assert!(!managarr_table.is_searching);
assert!(!managarr_table.search_produced_empty_results);
assert!(!managarr_table.is_filtering);
assert!(!managarr_table.filter_produced_empty_results);
assert_eq!(managarr_table.search_box_content_length, 0);
assert_eq!(managarr_table.search_box_offset, 0);
assert_eq!(managarr_table.filter_box_content_length, 0);
assert_eq!(managarr_table.filter_box_offset, 0);
}
#[test]
fn test_managarr_table_margin() {
let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(items.clone());
let managarr_table =
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])).margin(1);
let row_mapper = managarr_table.row_mapper;
assert_eq!(managarr_table.margin, 1);
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.footer, None);
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
assert_eq!(managarr_table.block, Block::new());
assert!(!managarr_table.is_loading);
assert!(managarr_table.highlight_rows);
assert!(!managarr_table.is_sorting);
assert!(!managarr_table.is_searching);
assert!(!managarr_table.search_produced_empty_results);
assert!(!managarr_table.is_filtering);
assert!(!managarr_table.filter_produced_empty_results);
assert_eq!(managarr_table.search_box_content_length, 0);
assert_eq!(managarr_table.search_box_offset, 0);
assert_eq!(managarr_table.filter_box_content_length, 0);
assert_eq!(managarr_table.filter_box_offset, 0);
}
#[test]
fn test_managarr_table_loading() {
let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(items.clone());
let managarr_table =
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)]))
.loading(true);
let row_mapper = managarr_table.row_mapper;
assert!(managarr_table.is_loading);
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.footer, None);
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
assert_eq!(managarr_table.block, Block::new());
assert_eq!(managarr_table.margin, 0);
assert!(managarr_table.highlight_rows);
assert!(!managarr_table.is_sorting);
assert!(!managarr_table.is_searching);
assert!(!managarr_table.search_produced_empty_results);
assert!(!managarr_table.is_filtering);
assert!(!managarr_table.filter_produced_empty_results);
assert_eq!(managarr_table.search_box_content_length, 0);
assert_eq!(managarr_table.search_box_offset, 0);
assert_eq!(managarr_table.filter_box_content_length, 0);
assert_eq!(managarr_table.filter_box_offset, 0);
}
#[test]
fn test_managarr_table_highlight_rows() {
let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(items.clone());
let managarr_table =
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)]))
.highlight_rows(false);
let row_mapper = managarr_table.row_mapper;
assert!(!managarr_table.highlight_rows);
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.footer, None);
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
assert_eq!(managarr_table.block, Block::new());
assert_eq!(managarr_table.margin, 0);
assert!(!managarr_table.is_loading);
assert!(!managarr_table.is_sorting);
assert!(!managarr_table.is_searching);
assert!(!managarr_table.search_produced_empty_results);
assert!(!managarr_table.is_filtering);
assert!(!managarr_table.filter_produced_empty_results);
assert_eq!(managarr_table.search_box_content_length, 0);
assert_eq!(managarr_table.search_box_offset, 0);
assert_eq!(managarr_table.filter_box_content_length, 0);
assert_eq!(managarr_table.filter_box_offset, 0);
}
#[test]
fn test_managarr_table_sorting() {
let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(items.clone());
let managarr_table =
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)]))
.sorting(true);
let row_mapper = managarr_table.row_mapper;
assert!(managarr_table.is_sorting);
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.footer, None);
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
assert_eq!(managarr_table.block, Block::new());
assert_eq!(managarr_table.margin, 0);
assert!(!managarr_table.is_loading);
assert!(managarr_table.highlight_rows);
assert!(!managarr_table.is_searching);
assert!(!managarr_table.search_produced_empty_results);
assert!(!managarr_table.is_filtering);
assert!(!managarr_table.filter_produced_empty_results);
assert_eq!(managarr_table.search_box_content_length, 0);
assert_eq!(managarr_table.search_box_offset, 0);
assert_eq!(managarr_table.filter_box_content_length, 0);
assert_eq!(managarr_table.filter_box_offset, 0);
}
#[test]
fn test_managarr_table_is_searching() {
let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(items.clone());
let managarr_table =
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)]))
.searching(true);
let row_mapper = managarr_table.row_mapper;
assert!(managarr_table.is_searching);
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.footer, None);
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
assert_eq!(managarr_table.block, Block::new());
assert_eq!(managarr_table.margin, 0);
assert!(!managarr_table.is_loading);
assert!(managarr_table.highlight_rows);
assert!(!managarr_table.is_sorting);
assert!(!managarr_table.search_produced_empty_results);
assert!(!managarr_table.is_filtering);
assert!(!managarr_table.filter_produced_empty_results);
assert_eq!(managarr_table.search_box_content_length, 0);
assert_eq!(managarr_table.search_box_offset, 0);
assert_eq!(managarr_table.filter_box_content_length, 0);
assert_eq!(managarr_table.filter_box_offset, 0);
}
#[test]
fn test_managarr_table_search_produced_empty_results() {
let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(items.clone());
let managarr_table =
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)]))
.search_produced_empty_results(true);
let row_mapper = managarr_table.row_mapper;
assert!(managarr_table.search_produced_empty_results);
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.footer, None);
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
assert_eq!(managarr_table.block, Block::new());
assert_eq!(managarr_table.margin, 0);
assert!(!managarr_table.is_loading);
assert!(managarr_table.highlight_rows);
assert!(!managarr_table.is_sorting);
assert!(!managarr_table.is_searching);
assert!(!managarr_table.is_filtering);
assert!(!managarr_table.filter_produced_empty_results);
assert_eq!(managarr_table.search_box_content_length, 0);
assert_eq!(managarr_table.search_box_offset, 0);
assert_eq!(managarr_table.filter_box_content_length, 0);
assert_eq!(managarr_table.filter_box_offset, 0);
}
#[test]
fn test_managarr_table_is_filtering() {
let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(items.clone());
let managarr_table =
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)]))
.filtering(true);
let row_mapper = managarr_table.row_mapper;
assert!(managarr_table.is_filtering);
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.footer, None);
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
assert_eq!(managarr_table.block, Block::new());
assert_eq!(managarr_table.margin, 0);
assert!(!managarr_table.is_loading);
assert!(managarr_table.highlight_rows);
assert!(!managarr_table.is_sorting);
assert!(!managarr_table.is_searching);
assert!(!managarr_table.search_produced_empty_results);
assert!(!managarr_table.filter_produced_empty_results);
assert_eq!(managarr_table.search_box_content_length, 0);
assert_eq!(managarr_table.search_box_offset, 0);
assert_eq!(managarr_table.filter_box_content_length, 0);
assert_eq!(managarr_table.filter_box_offset, 0);
}
#[test]
fn test_managarr_table_filter_produced_empty_results() {
let items = vec!["item1", "item2", "item3"];
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(items.clone());
let managarr_table =
ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)]))
.filter_produced_empty_results(true);
let row_mapper = managarr_table.row_mapper;
assert!(managarr_table.filter_produced_empty_results);
assert_eq!(managarr_table.content.unwrap().items, items);
assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")]));
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
assert_eq!(managarr_table.constraints, Vec::new());
assert_eq!(managarr_table.footer, None);
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
assert_eq!(managarr_table.block, Block::new());
assert_eq!(managarr_table.margin, 0);
assert!(!managarr_table.is_loading);
assert!(managarr_table.highlight_rows);
assert!(!managarr_table.is_sorting);
assert!(!managarr_table.is_searching);
assert!(!managarr_table.search_produced_empty_results);
assert!(!managarr_table.is_filtering);
assert_eq!(managarr_table.search_box_content_length, 0);
assert_eq!(managarr_table.search_box_offset, 0);
assert_eq!(managarr_table.filter_box_content_length, 0);
assert_eq!(managarr_table.filter_box_offset, 0);
}
#[test] #[test]
fn test_managarr_table_parse_headers() { fn test_managarr_table_parse_headers() {
let items = vec!["item1", "item2", "item3"]; let items = vec!["item1", "item2", "item3"];
+2 -15
View File
@@ -1,5 +1,6 @@
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::title_block_centered; use crate::ui::utils::title_block_centered;
use derive_setters::Setters;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Rect}; use ratatui::layout::{Alignment, Rect};
use ratatui::style::{Style, Stylize}; use ratatui::style::{Style, Stylize};
@@ -10,6 +11,7 @@ use ratatui::widgets::{Paragraph, Widget, Wrap};
#[path = "message_tests.rs"] #[path = "message_tests.rs"]
mod message_tests; mod message_tests;
#[derive(Setters)]
pub struct Message<'a> { pub struct Message<'a> {
text: Text<'a>, text: Text<'a>,
title: &'a str, title: &'a str,
@@ -30,21 +32,6 @@ impl<'a> Message<'a> {
} }
} }
pub fn title(mut self, title: &'a str) -> Self {
self.title = title;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
fn render_message(self, area: Rect, buf: &mut Buffer) { fn render_message(self, area: Rect, buf: &mut Buffer) {
Paragraph::new(self.text) Paragraph::new(self.text)
.style(self.style) .style(self.style)
-38
View File
@@ -18,42 +18,4 @@ mod tests {
assert_eq!(message.style, Style::new().failure().bold()); assert_eq!(message.style, Style::new().failure().bold());
assert_eq!(message.alignment, Alignment::Center); assert_eq!(message.alignment, Alignment::Center);
} }
#[test]
fn test_message_title() {
let test_message = "This is a message";
let title = "Success";
let message = Message::new(test_message).title(title);
assert_str_eq!(message.title, title);
assert_eq!(message.text, Text::from(test_message));
assert_eq!(message.style, Style::new().failure().bold());
assert_eq!(message.alignment, Alignment::Center);
}
#[test]
fn test_message_style() {
let test_message = "This is a message";
let style = Style::new().success().bold();
let message = Message::new(test_message).style(style);
assert_eq!(message.style, style);
assert_eq!(message.text, Text::from(test_message));
assert_str_eq!(message.title, "Error");
assert_eq!(message.alignment, Alignment::Center);
}
#[test]
fn test_message_alignment() {
let test_message = "This is a message";
let message = Message::new(test_message).alignment(Alignment::Left);
assert_eq!(message.alignment, Alignment::Left);
assert_eq!(message.text, Text::from(test_message));
assert_str_eq!(message.title, "Error");
assert_eq!(message.style, Style::new().failure().bold());
}
} }