Refactored table sorting into the ManagarrTable widget and StatefulTable so any and all tables created can support sorting with minimal UI changes and thus only need to focus on the handlers. I'm going to continue this effort tomorrow and look at what other widgets can be created to simplify things. Most likely, prompt boxes.

This commit is contained in:
2024-02-12 19:15:02 -07:00
parent adda82f7f3
commit 6ba78cb4ba
29 changed files with 1691 additions and 1716 deletions
@@ -6,7 +6,8 @@ use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, ADD_MOVIE_SELECTION_BLOCKS, COLLECTION_DETAILS_BLOCKS,
EDIT_COLLECTION_SELECTION_BLOCKS,
};
use crate::models::{BlockSelectionState, Scrollable, StatefulTable};
use crate::models::stateful_table::StatefulTable;
use crate::models::{BlockSelectionState, Scrollable};
#[cfg(test)]
#[path = "collection_details_handler_tests.rs"]
@@ -607,7 +607,7 @@ mod tests {
use ratatui::widgets::TableState;
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
use crate::models::StatefulTable;
use crate::models::stateful_table::StatefulTable;
use super::*;
@@ -733,7 +733,7 @@ mod tests {
RadarrData, EDIT_COLLECTION_SELECTION_BLOCKS,
};
use crate::models::StatefulTable;
use crate::models::stateful_table::StatefulTable;
use crate::{assert_refresh_key, test_edit_collection_key};
use super::*;
@@ -13,7 +13,7 @@ mod tests {
use rstest::rstest;
use crate::models::servarr_data::radarr::modals::IndexerTestResultModalItem;
use crate::models::StatefulTable;
use crate::models::stateful_table::StatefulTable;
use crate::simple_stateful_iterable_vec;
use super::*;
@@ -66,7 +66,7 @@ mod tests {
mod test_handle_home_end {
use crate::extended_stateful_iterable_vec;
use crate::models::servarr_data::radarr::modals::IndexerTestResultModalItem;
use crate::models::StatefulTable;
use crate::models::stateful_table::StatefulTable;
use pretty_assertions::assert_str_eq;
use super::*;
@@ -125,10 +125,9 @@ mod tests {
}
mod test_handle_esc {
use crate::models::stateful_table::StatefulTable;
use pretty_assertions::assert_eq;
use crate::models::StatefulTable;
use super::*;
#[test]
@@ -21,7 +21,8 @@ mod tests {
use crate::models::servarr_data::radarr::modals::AddMovieModal;
use crate::models::servarr_data::radarr::radarr_data::ADD_MOVIE_SELECTION_BLOCKS;
use crate::models::{BlockSelectionState, StatefulTable};
use crate::models::stateful_table::StatefulTable;
use crate::models::BlockSelectionState;
use crate::simple_stateful_iterable_vec;
use super::*;
@@ -348,7 +349,7 @@ mod tests {
use crate::extended_stateful_iterable_vec;
use crate::models::servarr_data::radarr::modals::AddMovieModal;
use crate::models::StatefulTable;
use crate::models::stateful_table::StatefulTable;
use super::*;
@@ -852,7 +853,8 @@ mod tests {
use crate::models::radarr_models::Movie;
use crate::models::servarr_data::radarr::modals::AddMovieModal;
use crate::models::servarr_data::radarr::radarr_data::ADD_MOVIE_SELECTION_BLOCKS;
use crate::models::{BlockSelectionState, StatefulTable};
use crate::models::stateful_table::StatefulTable;
use crate::models::BlockSelectionState;
use crate::network::radarr_network::RadarrEvent;
use super::*;
@@ -1146,7 +1148,7 @@ mod tests {
use crate::models::servarr_data::radarr::modals::AddMovieModal;
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
use crate::models::StatefulTable;
use crate::models::stateful_table::StatefulTable;
use crate::simple_stateful_iterable_vec;
use super::*;
@@ -586,7 +586,7 @@ mod tests {
use ratatui::widgets::TableState;
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
use crate::models::StatefulTable;
use crate::models::stateful_table::StatefulTable;
use super::*;
@@ -694,7 +694,7 @@ mod tests {
RadarrData, EDIT_MOVIE_SELECTION_BLOCKS,
};
use crate::models::StatefulTable;
use crate::models::stateful_table::StatefulTable;
use crate::{assert_refresh_key, test_edit_movie_key};
use super::*;
@@ -1,16 +1,14 @@
use std::cmp::Ordering;
use serde_json::Number;
use strum::IntoEnumIterator;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::{Language, Release, ReleaseField};
use crate::models::radarr_models::{Language, Release};
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, EDIT_MOVIE_SELECTION_BLOCKS, MOVIE_DETAILS_BLOCKS,
};
use crate::models::stateful_table::SortOption;
use crate::models::{BlockSelectionState, Scrollable};
use crate::network::radarr_network::RadarrEvent;
@@ -102,7 +100,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
.movie_details_modal
.as_mut()
.unwrap()
.movie_releases_sort
.movie_releases
.sort
.as_mut()
.unwrap()
.scroll_up(),
_ => (),
}
@@ -162,7 +163,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
.movie_details_modal
.as_mut()
.unwrap()
.movie_releases_sort
.movie_releases
.sort
.as_mut()
.unwrap()
.scroll_down(),
_ => (),
}
@@ -222,7 +226,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
.movie_details_modal
.as_mut()
.unwrap()
.movie_releases_sort
.movie_releases
.sort
.as_mut()
.unwrap()
.scroll_to_top(),
_ => (),
}
@@ -282,7 +289,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
.movie_details_modal
.as_mut()
.unwrap()
.movie_releases_sort
.movie_releases
.sort
.as_mut()
.unwrap()
.scroll_to_bottom(),
_ => (),
}
@@ -349,25 +359,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
self.app.pop_navigation_stack();
}
ActiveRadarrBlock::ManualSearchSortPrompt => {
let movie_details_modal = self
self
.app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap();
let movie_releases = movie_details_modal.movie_releases.items.clone();
let field = movie_details_modal.movie_releases_sort.current_selection();
let sort_ascending = !movie_details_modal.sort_ascending.unwrap();
movie_details_modal.sort_ascending = Some(sort_ascending);
movie_details_modal
.unwrap()
.movie_releases
.set_items(sort_releases_by_selected_field(
movie_releases,
*field,
sort_ascending,
));
.apply_sorting();
self.app.pop_navigation_stack();
}
_ => (),
@@ -433,19 +433,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
.pop_and_push_navigation_stack((*self.active_radarr_block).into());
}
_ if *key == DEFAULT_KEYBINDINGS.sort.key => {
let movie_details_modal = self
self
.app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap();
movie_details_modal
.movie_releases_sort
.set_items(Vec::from_iter(ReleaseField::iter()));
let sort_ascending = movie_details_modal.sort_ascending;
movie_details_modal.sort_ascending =
Some(sort_ascending.is_some() && sort_ascending.unwrap());
.unwrap()
.movie_releases
.sorting(releases_sorting_options());
self
.app
.push_navigation_stack(ActiveRadarrBlock::ManualSearchSortPrompt.into());
@@ -457,58 +453,67 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
}
}
fn sort_releases_by_selected_field(
mut releases: Vec<Release>,
field: ReleaseField,
sort_ascending: bool,
) -> Vec<Release> {
let cmp_fn: fn(&Release, &Release) -> Ordering = match field {
ReleaseField::Source => |release_a, release_b| release_a.protocol.cmp(&release_b.protocol),
ReleaseField::Age => |release_a, release_b| release_a.age.cmp(&release_b.age),
ReleaseField::Rejected => |release_a, release_b| release_a.rejected.cmp(&release_b.rejected),
ReleaseField::Title => |release_a, release_b| release_a.title.text.cmp(&release_b.title.text),
ReleaseField::Indexer => |release_a, release_b| release_a.indexer.cmp(&release_b.indexer),
ReleaseField::Size => |release_a, release_b| release_a.size.cmp(&release_b.size),
ReleaseField::Peers => |release_a, release_b| {
let default_number = Number::from(i64::MAX);
let seeder_a = release_a
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
let seeder_b = release_b
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
seeder_a.cmp(&seeder_b)
fn releases_sorting_options() -> Vec<SortOption<Release>> {
vec![
SortOption {
name: "Source",
cmp_fn: Some(|a, b| a.protocol.cmp(&b.protocol)),
},
ReleaseField::Language => |release_a, release_b| {
let default_language_vec = vec![Language {
name: "_".to_owned(),
}];
let language_a = &release_a
.languages
.as_ref()
.unwrap_or(&default_language_vec)[0];
let language_b = &release_b
.languages
.as_ref()
.unwrap_or(&default_language_vec)[0];
language_a.cmp(language_b)
SortOption {
name: "Age",
cmp_fn: Some(|a, b| a.age.cmp(&b.age)),
},
ReleaseField::Quality => |release_a, release_b| release_a.quality.cmp(&release_b.quality),
};
SortOption {
name: "Rejected",
cmp_fn: Some(|a, b| a.rejected.cmp(&b.rejected)),
},
SortOption {
name: "Title",
cmp_fn: Some(|a, b| a.title.text.cmp(&b.title.text)),
},
SortOption {
name: "Indexer",
cmp_fn: Some(|a, b| a.indexer.cmp(&b.indexer)),
},
SortOption {
name: "Size",
cmp_fn: Some(|a, b| a.size.cmp(&b.size)),
},
SortOption {
name: "Peers",
cmp_fn: Some(|a, b| {
let default_number = Number::from(i64::MAX);
let seeder_a = a
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
let seeder_b = b
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
if !sort_ascending {
releases.sort_by(|release_a, release_b| cmp_fn(release_a, release_b).reverse());
} else {
releases.sort_by(cmp_fn);
}
seeder_a.cmp(&seeder_b)
}),
},
SortOption {
name: "Language",
cmp_fn: Some(|a, b| {
let default_language_vec = vec![Language {
name: "_".to_owned(),
}];
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
let language_b = &b.languages.as_ref().unwrap_or(&default_language_vec)[0];
releases
language_a.cmp(language_b)
}),
},
SortOption {
name: "Quality",
cmp_fn: Some(|a, b| a.quality.cmp(&b.quality)),
},
]
}
@@ -1,7 +1,8 @@
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use pretty_assertions::assert_str_eq;
use rstest::rstest;
use serde_json::Number;
use strum::IntoEnumIterator;
@@ -9,21 +10,20 @@ mod tests {
use crate::app::App;
use crate::event::Key;
use crate::handlers::radarr_handlers::library::movie_details_handler::{
sort_releases_by_selected_field, MovieDetailsHandler,
releases_sorting_options, MovieDetailsHandler,
};
use crate::handlers::KeyEventHandler;
use crate::models::radarr_models::{
Credit, Language, MovieHistoryItem, Quality, QualityWrapper, Release, ReleaseField,
Credit, Language, MovieHistoryItem, Quality, QualityWrapper, Release,
};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS};
use crate::models::stateful_table::SortOption;
use crate::models::{HorizontallyScrollableText, ScrollableText};
mod test_handle_scroll_up_and_down {
use pretty_assertions::assert_eq;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::models::radarr_models::ReleaseField;
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::simple_stateful_iterable_vec;
@@ -261,12 +261,10 @@ mod tests {
fn test_manual_search_sort_scroll(
#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key,
) {
let release_field_vec = Vec::from_iter(ReleaseField::iter());
let release_field_vec = sort_options();
let mut app = App::default();
let mut movie_details_modal = MovieDetailsModal::default();
movie_details_modal
.movie_releases_sort
.set_items(release_field_vec.clone());
movie_details_modal.movie_releases.sorting(sort_options());
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
if key == Key::Up {
@@ -286,7 +284,10 @@ mod tests {
.movie_details_modal
.as_ref()
.unwrap()
.movie_releases_sort
.movie_releases
.sort
.as_ref()
.unwrap()
.current_selection(),
&release_field_vec[i]
);
@@ -308,7 +309,10 @@ mod tests {
.movie_details_modal
.as_ref()
.unwrap()
.movie_releases_sort
.movie_releases
.sort
.as_ref()
.unwrap()
.current_selection(),
&release_field_vec[(i + 1) % release_field_vec.len()]
);
@@ -318,10 +322,7 @@ mod tests {
}
mod test_handle_home_end {
use strum::IntoEnumIterator;
use crate::extended_stateful_iterable_vec;
use crate::models::radarr_models::ReleaseField;
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use super::*;
@@ -597,12 +598,10 @@ mod tests {
#[test]
fn test_manual_search_sort_home_end() {
let release_field_vec = Vec::from_iter(ReleaseField::iter());
let release_field_vec = sort_options();
let mut app = App::default();
let mut movie_details_modal = MovieDetailsModal::default();
movie_details_modal
.movie_releases_sort
.set_items(release_field_vec.clone());
movie_details_modal.movie_releases.sorting(sort_options());
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
MovieDetailsHandler::with(
@@ -620,7 +619,10 @@ mod tests {
.movie_details_modal
.as_ref()
.unwrap()
.movie_releases_sort
.movie_releases
.sort
.as_ref()
.unwrap()
.current_selection(),
&release_field_vec[release_field_vec.len() - 1]
);
@@ -640,7 +642,10 @@ mod tests {
.movie_details_modal
.as_ref()
.unwrap()
.movie_releases_sort
.movie_releases
.sort
.as_ref()
.unwrap()
.current_selection(),
&release_field_vec[0]
);
@@ -720,7 +725,6 @@ mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::models::radarr_models::ReleaseField;
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::network::radarr_network::RadarrEvent;
@@ -805,13 +809,9 @@ mod tests {
#[test]
fn test_manual_search_sort_prompt_submit() {
let mut app = App::default();
let mut movie_details_modal = MovieDetailsModal {
sort_ascending: Some(true),
..MovieDetailsModal::default()
};
movie_details_modal
.movie_releases_sort
.set_items(vec![ReleaseField::default()]);
let mut movie_details_modal = MovieDetailsModal::default();
movie_details_modal.movie_releases.sort_asc = true;
movie_details_modal.movie_releases.sorting(sort_options());
movie_details_modal.movie_releases.set_items(release_vec());
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
app.push_navigation_stack(ActiveRadarrBlock::ManualSearch.into());
@@ -908,13 +908,13 @@ mod tests {
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::handlers::radarr_handlers::library::movie_details_handler::releases_sorting_options;
use crate::models::radarr_models::{MinimumAvailability, Movie};
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
use crate::models::servarr_data::radarr::radarr_data::{
RadarrData, EDIT_MOVIE_SELECTION_BLOCKS,
};
use crate::test_edit_movie_key;
use super::*;
@@ -964,23 +964,6 @@ mod tests {
app.get_current_route(),
&ActiveRadarrBlock::ManualSearchSortPrompt.into()
);
assert!(!app
.data
.radarr_data
.movie_details_modal
.as_ref()
.unwrap()
.movie_releases_sort
.items
.is_empty());
assert!(app
.data
.radarr_data
.movie_details_modal
.as_ref()
.unwrap()
.sort_ascending
.is_some());
assert_eq!(
app
.data
@@ -988,8 +971,22 @@ mod tests {
.movie_details_modal
.as_ref()
.unwrap()
.sort_ascending,
Some(false)
.movie_releases
.sort
.as_ref()
.unwrap()
.items,
releases_sorting_options()
);
assert!(
!app
.data
.radarr_data
.movie_details_modal
.as_ref()
.unwrap()
.movie_releases
.sort_asc
);
}
@@ -1067,46 +1064,155 @@ mod tests {
}
}
#[rstest]
fn test_sort_releases_by_selected_field(
#[values(
ReleaseField::Source,
ReleaseField::Age,
ReleaseField::Title,
ReleaseField::Indexer,
ReleaseField::Size,
ReleaseField::Peers,
ReleaseField::Language,
ReleaseField::Quality
)]
field: ReleaseField,
) {
let mut expected_vec = release_vec();
#[test]
fn test_releases_sorting_options_source() {
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.protocol.cmp(&b.protocol);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sorted_releases = sort_releases_by_selected_field(release_vec(), field, true);
let sort_option = releases_sorting_options()[0].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases, expected_vec);
let sorted_releases = sort_releases_by_selected_field(release_vec(), field, false);
expected_vec.reverse();
assert_eq!(sorted_releases, expected_vec);
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Source");
}
#[test]
fn test_sort_releases_by_selected_field_rejected() {
let mut expected_vec = Vec::from(&release_vec()[1..]);
expected_vec.push(release_vec()[0].clone());
fn test_releases_sorting_options_age() {
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.age.cmp(&b.age);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sorted_releases =
sort_releases_by_selected_field(release_vec(), ReleaseField::Rejected, true);
let sort_option = releases_sorting_options()[1].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases, expected_vec);
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Age");
}
let sorted_releases =
sort_releases_by_selected_field(release_vec(), ReleaseField::Rejected, false);
#[test]
fn test_releases_sorting_options_rejected() {
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.rejected.cmp(&b.rejected);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
assert_eq!(sorted_releases, release_vec());
let sort_option = releases_sorting_options()[2].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Rejected");
}
#[test]
fn test_releases_sorting_options_title() {
let expected_cmp_fn: fn(&Release, &Release) -> Ordering =
|a, b| a.title.text.cmp(&b.title.text);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[3].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Title");
}
#[test]
fn test_releases_sorting_options_indexer() {
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.indexer.cmp(&b.indexer);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[4].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Indexer");
}
#[test]
fn test_releases_sorting_options_size() {
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.size.cmp(&b.size);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[5].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Size");
}
#[test]
fn test_releases_sorting_options_peers() {
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| {
let default_number = Number::from(i64::MAX);
let seeder_a = a
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
let seeder_b = b
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
seeder_a.cmp(&seeder_b)
};
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[6].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Peers");
}
#[test]
fn test_releases_sorting_options_language() {
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| {
let default_language_vec = vec![Language {
name: "_".to_owned(),
}];
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
let language_b = &b.languages.as_ref().unwrap_or(&default_language_vec)[0];
language_a.cmp(language_b)
};
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[7].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Language");
}
#[test]
fn test_releases_sorting_options_quality() {
let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.quality.cmp(&b.quality);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[8].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Quality");
}
fn release_vec() -> Vec<Release> {
@@ -1166,6 +1272,13 @@ mod tests {
vec![release_a, release_b, release_c]
}
fn sort_options() -> Vec<SortOption<Release>> {
vec![SortOption {
name: "Test 1",
cmp_fn: Some(|a, b| a.age.cmp(&b.age)),
}]
}
#[test]
fn test_movie_details_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
@@ -3,7 +3,8 @@ use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS};
use crate::models::{Scrollable, StatefulList};
use crate::models::stateful_list::StatefulList;
use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent;
#[cfg(test)]
+2 -258
View File
@@ -2,267 +2,14 @@ use std::cell::RefCell;
use std::fmt::{Debug, Display, Formatter};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use ratatui::widgets::{ListState, TableState};
use regex::Regex;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Number;
pub mod radarr_models;
pub mod servarr_data;
macro_rules! stateful_iterable {
($name:ident, $state:ty) => {
#[derive(Default)]
pub struct $name<T> {
pub state: $state,
pub items: Vec<T>,
pub filter: Option<HorizontallyScrollableText>,
pub search: Option<HorizontallyScrollableText>,
pub filtered_items: Option<Vec<T>>,
pub filtered_state: Option<$state>,
}
impl<T> Scrollable for $name<T> {
fn scroll_down(&mut self) {
if let Some(filtered_items) = self.filtered_items.as_ref() {
if filtered_items.is_empty() {
return;
}
let selected_row = match self.filtered_state.as_ref().unwrap().selected() {
Some(i) => {
if i >= filtered_items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self
.filtered_state
.as_mut()
.unwrap()
.select(Some(selected_row));
return;
}
if self.items.is_empty() {
return;
}
let selected_row = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(selected_row));
}
fn scroll_up(&mut self) {
if let Some(filtered_items) = self.filtered_items.as_ref() {
if filtered_items.is_empty() {
return;
}
let selected_row = match self.filtered_state.as_ref().unwrap().selected() {
Some(i) => {
if i == 0 {
filtered_items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self
.filtered_state
.as_mut()
.unwrap()
.select(Some(selected_row));
return;
}
if self.items.is_empty() {
return;
}
let selected_row = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(selected_row));
}
fn scroll_to_top(&mut self) {
if let Some(filtered_items) = self.filtered_items.as_ref() {
if filtered_items.is_empty() {
return;
}
self.filtered_state.as_mut().unwrap().select(Some(0));
return;
}
if self.items.is_empty() {
return;
}
self.state.select(Some(0));
}
fn scroll_to_bottom(&mut self) {
if let Some(filtered_items) = self.filtered_items.as_ref() {
if filtered_items.is_empty() {
return;
}
self
.filtered_state
.as_mut()
.unwrap()
.select(Some(filtered_items.len() - 1));
return;
}
if self.items.is_empty() {
return;
}
self.state.select(Some(self.items.len() - 1));
}
}
#[allow(dead_code)]
impl<T> $name<T>
where
T: Clone + PartialEq + Eq + Debug,
{
pub fn set_items(&mut self, items: Vec<T>) {
let items_len = items.len();
self.items = items;
if !self.items.is_empty() {
let selected_row = self.state.selected().map_or(0, |i| {
if i > 0 && i < items_len {
i
} else if i >= items_len {
items_len - 1
} else {
0
}
});
self.state.select(Some(selected_row));
}
}
pub fn set_filtered_items(&mut self, filtered_items: Vec<T>) {
self.filtered_items = Some(filtered_items);
let mut filtered_state: $state = Default::default();
filtered_state.select(Some(0));
self.filtered_state = Some(filtered_state);
}
pub fn select_index(&mut self, index: Option<usize>) {
if let Some(filtered_state) = &mut self.filtered_state {
filtered_state.select(index);
} else {
self.state.select(index);
}
}
pub fn current_selection(&self) -> &T {
if let Some(filtered_items) = &self.filtered_items {
&filtered_items[self
.filtered_state
.as_ref()
.unwrap()
.selected()
.unwrap_or(0)]
} else {
&self.items[self.state.selected().unwrap_or(0)]
}
}
pub fn apply_filter(&mut self, filter_field: fn(&T) -> &str) -> bool {
let filter_matches = match self.filter {
Some(ref filter) if !filter.text.is_empty() => {
let scrubbed_filter = strip_non_search_characters(&filter.text.clone());
self
.items
.iter()
.filter(|item| {
strip_non_search_characters(filter_field(&item)).contains(&scrubbed_filter)
})
.cloned()
.collect()
}
_ => Vec::new(),
};
self.filter = None;
if filter_matches.is_empty() {
return false;
}
self.set_filtered_items(filter_matches);
return true;
}
pub fn reset_filter(&mut self) {
self.filter = None;
self.filtered_items = None;
self.filtered_state = None;
}
pub fn apply_search(&mut self, search_field: fn(&T) -> &str) -> bool {
let search_index = if let Some(search) = self.search.as_ref() {
let search_string = search.text.clone().to_lowercase();
self
.filtered_items
.as_ref()
.unwrap_or(&self.items)
.iter()
.position(|item| {
strip_non_search_characters(search_field(&item)).contains(&search_string)
})
} else {
None
};
self.search = None;
if search_index.is_none() {
return false;
}
self.select_index(search_index);
return true;
}
pub fn reset_search(&mut self) {
self.search = None;
}
}
};
}
pub mod stateful_list;
pub mod stateful_table;
#[cfg(test)]
#[path = "model_tests.rs"]
@@ -289,9 +36,6 @@ pub trait Scrollable {
fn scroll_to_bottom(&mut self);
}
stateful_iterable!(StatefulList, ListState);
stateful_iterable!(StatefulTable, TableState);
#[derive(Default)]
pub struct ScrollableText {
pub items: Vec<String>,
+1 -965
View File
@@ -3,7 +3,6 @@ mod tests {
use std::cell::RefCell;
use pretty_assertions::{assert_eq, assert_str_eq};
use ratatui::widgets::{ListState, TableState};
use serde::de::value::Error as ValueError;
use serde::de::value::F64Deserializer;
use serde::de::value::I64Deserializer;
@@ -13,8 +12,7 @@ mod tests {
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::{from_i64, strip_non_search_characters};
use crate::models::{
BlockSelectionState, HorizontallyScrollableText, Scrollable, ScrollableText, StatefulList,
StatefulTable, TabRoute, TabState,
BlockSelectionState, HorizontallyScrollableText, Scrollable, ScrollableText, TabRoute, TabState,
};
const BLOCKS: [ActiveRadarrBlock; 6] = [
@@ -26,940 +24,6 @@ mod tests {
ActiveRadarrBlock::AddMovieConfirmPrompt,
];
#[test]
fn test_stateful_table_scrolling_on_empty_table_performs_no_op() {
let mut stateful_table: StatefulTable<String> = StatefulTable::default();
assert_eq!(stateful_table.state.selected(), None);
stateful_table.scroll_up();
assert_eq!(stateful_table.state.selected(), None);
stateful_table.scroll_down();
assert_eq!(stateful_table.state.selected(), None);
stateful_table.scroll_to_top();
assert_eq!(stateful_table.state.selected(), None);
stateful_table.scroll_to_bottom();
}
#[test]
fn test_stateful_table_filtered_scrolling_on_empty_table_performs_no_op() {
let mut filtered_stateful_table: StatefulTable<String> = StatefulTable {
filtered_items: Some(Vec::new()),
filtered_state: Some(TableState::default()),
..StatefulTable::default()
};
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
filtered_stateful_table.scroll_up();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
filtered_stateful_table.scroll_down();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
filtered_stateful_table.scroll_to_top();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
filtered_stateful_table.scroll_to_bottom();
}
#[test]
fn test_stateful_table_scroll() {
let mut stateful_table = create_test_stateful_table();
assert_eq!(stateful_table.state.selected(), Some(0));
stateful_table.scroll_down();
assert_eq!(stateful_table.state.selected(), Some(1));
stateful_table.scroll_down();
assert_eq!(stateful_table.state.selected(), Some(0));
stateful_table.scroll_up();
assert_eq!(stateful_table.state.selected(), Some(1));
stateful_table.scroll_up();
assert_eq!(stateful_table.state.selected(), Some(0));
stateful_table.scroll_to_bottom();
assert_eq!(stateful_table.state.selected(), Some(1));
stateful_table.scroll_to_top();
assert_eq!(stateful_table.state.selected(), Some(0));
}
#[test]
fn test_stateful_table_filtered_items_scroll() {
let mut filtered_stateful_table = create_test_filtered_stateful_table();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_table.scroll_down();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_table.scroll_down();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_table.scroll_up();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_table.scroll_up();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_table.scroll_to_bottom();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_table.scroll_to_top();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
}
#[test]
fn test_stateful_table_set_items() {
let items_vec = vec!["Test 1", "Test 2", "Test 3"];
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_items(items_vec.clone());
assert_eq!(stateful_table.state.selected(), Some(0));
stateful_table.state.select(Some(1));
stateful_table.set_items(items_vec.clone());
assert_eq!(stateful_table.state.selected(), Some(1));
stateful_table.state.select(Some(3));
stateful_table.set_items(items_vec);
assert_eq!(stateful_table.state.selected(), Some(2));
}
#[test]
fn test_stateful_table_set_filtered_items() {
let filtered_items_vec = vec!["Test 1", "Test 2", "Test 3"];
let mut filtered_stateful_table: StatefulTable<&str> = StatefulTable::default();
filtered_stateful_table.set_filtered_items(filtered_items_vec.clone());
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
assert_eq!(
filtered_stateful_table.filtered_items,
Some(filtered_items_vec.clone())
);
}
#[test]
fn test_stateful_table_current_selection() {
let mut stateful_table = create_test_stateful_table();
assert_str_eq!(stateful_table.current_selection(), &stateful_table.items[0]);
stateful_table.state.select(Some(1));
assert_str_eq!(stateful_table.current_selection(), &stateful_table.items[1]);
}
#[test]
fn test_filtered_stateful_table_current_selection() {
let mut filtered_stateful_table = create_test_filtered_stateful_table();
assert_str_eq!(
filtered_stateful_table.current_selection(),
&filtered_stateful_table.filtered_items.as_ref().unwrap()[0]
);
filtered_stateful_table
.filtered_state
.as_mut()
.unwrap()
.select(Some(1));
assert_str_eq!(
filtered_stateful_table.current_selection(),
&filtered_stateful_table.filtered_items.as_ref().unwrap()[1]
);
}
#[test]
fn test_stateful_table_select_index() {
let mut stateful_table = create_test_stateful_table();
assert_eq!(stateful_table.state.selected(), Some(0));
stateful_table.select_index(Some(1));
assert_eq!(stateful_table.state.selected(), Some(1));
stateful_table.select_index(None);
assert_eq!(stateful_table.state.selected(), None);
}
#[test]
fn test_filtered_stateful_table_select_index() {
let mut filtered_stateful_table = create_test_filtered_stateful_table();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_table.select_index(Some(1));
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_table.select_index(None);
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
}
#[test]
fn test_stateful_table_scroll_up() {
let mut stateful_table = create_test_stateful_table();
assert_eq!(stateful_table.state.selected(), Some(0));
stateful_table.scroll_up();
assert_eq!(stateful_table.state.selected(), Some(1));
stateful_table.scroll_up();
assert_eq!(stateful_table.state.selected(), Some(0));
}
#[test]
fn test_filtered_stateful_table_scroll_up() {
let mut filtered_stateful_table = create_test_filtered_stateful_table();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_table.scroll_up();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_table.scroll_up();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
}
#[test]
fn test_stateful_table_apply_filter() {
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_items(vec!["this", "is", "a", "test"]);
stateful_table.filter = Some("i".into());
let expected_items = vec!["this", "is"];
let mut expected_state = TableState::default();
expected_state.select(Some(0));
let has_matches = stateful_table.apply_filter(|&item| item);
assert_eq!(stateful_table.filter, None);
assert_eq!(stateful_table.filtered_items, Some(expected_items));
assert_eq!(stateful_table.filtered_state, Some(expected_state));
assert!(has_matches);
}
#[test]
fn test_stateful_table_apply_filter_no_matches() {
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_items(vec!["this", "is", "a", "test"]);
stateful_table.filter = Some("z".into());
let has_matches = stateful_table.apply_filter(|&item| item);
assert_eq!(stateful_table.filter, None);
assert_eq!(stateful_table.filtered_items, None);
assert_eq!(stateful_table.filtered_state, None);
assert!(!has_matches);
}
#[test]
fn test_stateful_table_reset_filter() {
let mut stateful_table = create_test_filtered_stateful_table();
stateful_table.reset_filter();
assert_eq!(stateful_table.filter, None);
assert_eq!(stateful_table.filtered_items, None);
assert_eq!(stateful_table.filtered_state, None);
}
#[test]
fn test_stateful_table_apply_search() {
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_items(vec!["this", "is", "a", "test"]);
stateful_table.search = Some("test".into());
let mut expected_state = TableState::default();
expected_state.select(Some(3));
let has_match = stateful_table.apply_search(|&item| item);
assert_eq!(stateful_table.search, None);
assert_eq!(stateful_table.state, expected_state);
assert!(has_match);
}
#[test]
fn test_stateful_table_apply_search_no_match() {
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_items(vec!["this", "is", "a", "test"]);
stateful_table.search = Some("shi-mon-a!".into());
let has_match = stateful_table.apply_search(|&item| item);
assert_eq!(stateful_table.search, None);
assert!(!has_match);
}
#[test]
fn test_filtered_stateful_table_apply_search() {
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_filtered_items(vec!["this", "is", "a", "test"]);
stateful_table.search = Some("test".into());
let mut expected_state = TableState::default();
expected_state.select(Some(3));
let has_match = stateful_table.apply_search(|&item| item);
assert_eq!(stateful_table.search, None);
assert_eq!(stateful_table.filtered_state, Some(expected_state));
assert!(has_match);
}
#[test]
fn test_filtered_stateful_table_apply_search_no_match() {
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_filtered_items(vec!["this", "is", "a", "test"]);
stateful_table.search = Some("shi-mon-a!".into());
let mut expected_state = TableState::default();
expected_state.select(Some(0));
let has_match = stateful_table.apply_search(|&item| item);
assert_eq!(stateful_table.search, None);
assert_eq!(stateful_table.filtered_state, Some(expected_state));
assert!(!has_match);
}
#[test]
fn test_stateful_table_reset_search() {
let mut stateful_table = create_test_stateful_table();
stateful_table.search = Some("test".into());
stateful_table.reset_search();
assert_eq!(stateful_table.search, None);
}
#[test]
fn test_stateful_list_scrolling_on_empty_list_performs_no_op() {
let mut stateful_list: StatefulList<String> = StatefulList::default();
assert_eq!(stateful_list.state.selected(), None);
stateful_list.scroll_up();
assert_eq!(stateful_list.state.selected(), None);
stateful_list.scroll_down();
assert_eq!(stateful_list.state.selected(), None);
stateful_list.scroll_to_top();
assert_eq!(stateful_list.state.selected(), None);
stateful_list.scroll_to_bottom();
}
#[test]
fn test_filtered_stateful_list_scrolling_on_empty_list_performs_no_op() {
let mut filtered_stateful_list: StatefulList<String> = StatefulList {
filtered_items: Some(Vec::new()),
filtered_state: Some(ListState::default()),
..StatefulList::default()
};
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
filtered_stateful_list.scroll_up();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
filtered_stateful_list.scroll_down();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
filtered_stateful_list.scroll_to_top();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
filtered_stateful_list.scroll_to_bottom();
}
#[test]
fn test_stateful_list_scroll() {
let mut stateful_list = create_test_stateful_list();
assert_eq!(stateful_list.state.selected(), Some(0));
stateful_list.scroll_down();
assert_eq!(stateful_list.state.selected(), Some(1));
stateful_list.scroll_down();
assert_eq!(stateful_list.state.selected(), Some(0));
stateful_list.scroll_up();
assert_eq!(stateful_list.state.selected(), Some(1));
stateful_list.scroll_up();
assert_eq!(stateful_list.state.selected(), Some(0));
stateful_list.scroll_to_bottom();
assert_eq!(stateful_list.state.selected(), Some(1));
stateful_list.scroll_to_top();
assert_eq!(stateful_list.state.selected(), Some(0));
}
#[test]
fn test_filtered_stateful_list_scroll() {
let mut filtered_stateful_list = create_test_filtered_stateful_list();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_list.scroll_down();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_list.scroll_down();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_list.scroll_up();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_list.scroll_up();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_list.scroll_to_bottom();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_list.scroll_to_top();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
}
#[test]
fn test_stateful_list_set_items() {
let items_vec = vec!["Test 1", "Test 2", "Test 3"];
let mut stateful_list: StatefulList<&str> = StatefulList::default();
stateful_list.set_items(items_vec.clone());
assert_eq!(stateful_list.state.selected(), Some(0));
stateful_list.state.select(Some(1));
stateful_list.set_items(items_vec.clone());
assert_eq!(stateful_list.state.selected(), Some(1));
stateful_list.state.select(Some(3));
stateful_list.set_items(items_vec);
assert_eq!(stateful_list.state.selected(), Some(2));
}
#[test]
fn test_stateful_list_set_filtered_items() {
let filtered_items_vec = vec!["Test 1", "Test 2", "Test 3"];
let mut filtered_stateful_list: StatefulList<&str> = StatefulList::default();
filtered_stateful_list.set_filtered_items(filtered_items_vec.clone());
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
assert_eq!(
filtered_stateful_list.filtered_items,
Some(filtered_items_vec.clone())
);
}
#[test]
fn test_stateful_list_current_selection() {
let mut stateful_list = create_test_stateful_list();
assert_str_eq!(stateful_list.current_selection(), &stateful_list.items[0]);
stateful_list.state.select(Some(1));
assert_str_eq!(stateful_list.current_selection(), &stateful_list.items[1]);
}
#[test]
fn test_filtered_stateful_list_current_selection() {
let mut filtered_stateful_list = create_test_filtered_stateful_list();
assert_str_eq!(
filtered_stateful_list.current_selection(),
&filtered_stateful_list.filtered_items.as_ref().unwrap()[0]
);
filtered_stateful_list
.filtered_state
.as_mut()
.unwrap()
.select(Some(1));
assert_str_eq!(
filtered_stateful_list.current_selection(),
&filtered_stateful_list.filtered_items.as_ref().unwrap()[1]
);
}
#[test]
fn test_stateful_list_select_index() {
let mut stateful_list = create_test_stateful_list();
assert_eq!(stateful_list.state.selected(), Some(0));
stateful_list.select_index(Some(1));
assert_eq!(stateful_list.state.selected(), Some(1));
stateful_list.select_index(None);
assert_eq!(stateful_list.state.selected(), None);
}
#[test]
fn test_filtered_stateful_list_select_index() {
let mut filtered_stateful_list = create_test_filtered_stateful_list();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_list.select_index(Some(1));
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_list.select_index(None);
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
}
#[test]
fn test_stateful_list_scroll_up() {
let mut stateful_list = create_test_stateful_list();
assert_eq!(stateful_list.state.selected(), Some(0));
stateful_list.scroll_up();
assert_eq!(stateful_list.state.selected(), Some(1));
stateful_list.scroll_up();
assert_eq!(stateful_list.state.selected(), Some(0));
}
#[test]
fn test_filtered_stateful_list_scroll_up() {
let mut filtered_stateful_list = create_test_filtered_stateful_list();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_list.scroll_up();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_list.scroll_up();
assert_eq!(
filtered_stateful_list
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
}
#[test]
fn test_stateful_list_apply_filter() {
let mut stateful_list: StatefulList<&str> = StatefulList::default();
stateful_list.set_items(vec!["this", "is", "a", "test"]);
stateful_list.filter = Some("i".into());
let expected_items = vec!["this", "is"];
let mut expected_state = ListState::default();
expected_state.select(Some(0));
let has_matches = stateful_list.apply_filter(|&item| item);
assert_eq!(stateful_list.filter, None);
assert_eq!(stateful_list.filtered_items, Some(expected_items));
assert_eq!(stateful_list.filtered_state, Some(expected_state));
assert!(has_matches);
}
#[test]
fn test_stateful_list_apply_filter_no_matches() {
let mut stateful_list: StatefulList<&str> = StatefulList::default();
stateful_list.set_items(vec!["this", "is", "a", "test"]);
stateful_list.filter = Some("z".into());
let has_matches = stateful_list.apply_filter(|&item| item);
assert_eq!(stateful_list.filter, None);
assert_eq!(stateful_list.filtered_items, None);
assert_eq!(stateful_list.filtered_state, None);
assert!(!has_matches);
}
#[test]
fn test_stateful_list_reset_filter() {
let mut stateful_list = create_test_filtered_stateful_list();
stateful_list.reset_filter();
assert_eq!(stateful_list.filter, None);
assert_eq!(stateful_list.filtered_items, None);
assert_eq!(stateful_list.filtered_state, None);
}
#[test]
fn test_stateful_list_apply_search() {
let mut stateful_list: StatefulList<&str> = StatefulList::default();
stateful_list.set_items(vec!["this", "is", "a", "test"]);
stateful_list.search = Some("test".into());
let mut expected_state = ListState::default();
expected_state.select(Some(3));
let has_match = stateful_list.apply_search(|&item| item);
assert_eq!(stateful_list.search, None);
assert_eq!(stateful_list.state, expected_state);
assert!(has_match);
}
#[test]
fn test_stateful_list_apply_search_no_match() {
let mut stateful_list: StatefulList<&str> = StatefulList::default();
stateful_list.set_items(vec!["this", "is", "a", "test"]);
stateful_list.search = Some("shi-mon-a!".into());
let has_match = stateful_list.apply_search(|&item| item);
assert_eq!(stateful_list.search, None);
assert!(!has_match);
}
#[test]
fn test_filtered_stateful_list_apply_search() {
let mut stateful_list: StatefulList<&str> = StatefulList::default();
stateful_list.set_filtered_items(vec!["this", "is", "a", "test"]);
stateful_list.search = Some("test".into());
let mut expected_state = ListState::default();
expected_state.select(Some(3));
let has_match = stateful_list.apply_search(|&item| item);
assert_eq!(stateful_list.search, None);
assert_eq!(stateful_list.filtered_state, Some(expected_state));
assert!(has_match);
}
#[test]
fn test_filtered_stateful_list_apply_search_no_match() {
let mut stateful_list: StatefulList<&str> = StatefulList::default();
stateful_list.set_filtered_items(vec!["this", "is", "a", "test"]);
stateful_list.search = Some("shi-mon-a!".into());
let mut expected_state = ListState::default();
expected_state.select(Some(0));
let has_match = stateful_list.apply_search(|&item| item);
assert_eq!(stateful_list.search, None);
assert_eq!(stateful_list.filtered_state, Some(expected_state));
assert!(!has_match);
}
#[test]
fn test_stateful_list_reset_search() {
let mut stateful_list = create_test_stateful_list();
stateful_list.search = Some("test".into());
stateful_list.reset_search();
assert_eq!(stateful_list.search, None);
}
#[test]
fn test_scrollable_text_with_string() {
let scrollable_text = ScrollableText::with_string("Test \n String \n".to_owned());
@@ -1488,34 +552,6 @@ mod tests {
]
}
fn create_test_stateful_table() -> StatefulTable<&'static str> {
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(vec!["Test 1", "Test 2"]);
stateful_table
}
fn create_test_filtered_stateful_table() -> StatefulTable<&'static str> {
let mut stateful_table = StatefulTable::default();
stateful_table.set_filtered_items(vec!["Test 1", "Test 2"]);
stateful_table
}
fn create_test_stateful_list() -> StatefulList<&'static str> {
let mut stateful_list = StatefulList::default();
stateful_list.set_items(vec!["Test 1", "Test 2"]);
stateful_list
}
fn create_test_filtered_stateful_list() -> StatefulList<&'static str> {
let mut stateful_list = StatefulList::default();
stateful_list.set_filtered_items(vec!["Test 1", "Test 2"]);
stateful_list
}
#[test]
fn test_strip_non_alphanumeric_characters() {
assert_eq!(
+1 -15
View File
@@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};
use derivative::Derivative;
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};
use strum_macros::{Display, EnumIter};
use strum_macros::EnumIter;
use crate::models::HorizontallyScrollableText;
@@ -454,20 +454,6 @@ pub struct ReleaseDownloadBody {
pub movie_id: i64,
}
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, Display)]
pub enum ReleaseField {
#[default]
Source,
Age,
Rejected,
Title,
Indexer,
Size,
Peers,
Language,
Quality,
}
#[derive(Default, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RootFolder {
+6 -5
View File
@@ -1,10 +1,13 @@
use strum::IntoEnumIterator;
use crate::models::radarr_models::{
Collection, Credit, Indexer, MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release,
ReleaseField, RootFolder,
RootFolder,
};
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
use crate::models::{HorizontallyScrollableText, ScrollableText, StatefulList, StatefulTable};
use strum::IntoEnumIterator;
use crate::models::stateful_list::StatefulList;
use crate::models::stateful_table::StatefulTable;
use crate::models::{HorizontallyScrollableText, ScrollableText};
#[cfg(test)]
#[path = "modals_tests.rs"]
@@ -20,8 +23,6 @@ pub struct MovieDetailsModal {
pub movie_cast: StatefulTable<Credit>,
pub movie_crew: StatefulTable<Credit>,
pub movie_releases: StatefulTable<Release>,
pub movie_releases_sort: StatefulList<ReleaseField>,
pub sort_ascending: Option<bool>,
}
#[derive(Default, Debug, PartialEq, Eq)]
@@ -8,7 +8,7 @@ mod test {
};
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
use crate::models::StatefulTable;
use crate::models::stateful_table::StatefulTable;
use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
@@ -13,9 +13,10 @@ use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
MovieDetailsModal,
};
use crate::models::stateful_list::StatefulList;
use crate::models::stateful_table::StatefulTable;
use crate::models::{
BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, StatefulList,
StatefulTable, TabRoute, TabState,
BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, TabRoute, TabState,
};
use crate::network::radarr_network::RadarrEvent;
use bimap::BiMap;
@@ -1,11 +1,12 @@
#[cfg(test)]
pub mod utils {
use crate::models::radarr_models::{
AddMovieSearchResult, CollectionMovie, Credit, MovieHistoryItem, Release, ReleaseField,
AddMovieSearchResult, CollectionMovie, Credit, MovieHistoryItem, Release,
};
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
use crate::models::{HorizontallyScrollableText, ScrollableText, StatefulTable};
use crate::models::stateful_table::StatefulTable;
use crate::models::{HorizontallyScrollableText, ScrollableText};
pub fn create_test_radarr_data<'a>() -> RadarrData<'a> {
let mut movie_details_modal = MovieDetailsModal {
@@ -24,10 +25,6 @@ pub mod utils {
movie_details_modal
.movie_releases
.set_items(vec![Release::default()]);
movie_details_modal
.movie_releases_sort
.set_items(vec![ReleaseField::default()]);
movie_details_modal.sort_ascending = Some(true);
let mut radarr_data = RadarrData {
delete_movie_files: true,
+95
View File
@@ -0,0 +1,95 @@
use crate::models::Scrollable;
use ratatui::widgets::ListState;
use std::fmt::Debug;
#[cfg(test)]
#[path = "stateful_list_tests.rs"]
mod stateful_list_tests;
#[derive(Default)]
pub struct StatefulList<T> {
pub state: ListState,
pub items: Vec<T>,
}
impl<T> Scrollable for StatefulList<T> {
fn scroll_down(&mut self) {
if self.items.is_empty() {
return;
}
let selected_row = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(selected_row));
}
fn scroll_up(&mut self) {
if self.items.is_empty() {
return;
}
let selected_row = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(selected_row));
}
fn scroll_to_top(&mut self) {
if self.items.is_empty() {
return;
}
self.state.select(Some(0));
}
fn scroll_to_bottom(&mut self) {
if self.items.is_empty() {
return;
}
self.state.select(Some(self.items.len() - 1));
}
}
impl<T> StatefulList<T>
where
T: Clone + PartialEq + Eq + Debug,
{
pub fn set_items(&mut self, items: Vec<T>) {
let items_len = items.len();
self.items = items;
if !self.items.is_empty() {
let selected_row = self.state.selected().map_or(0, |i| {
if i > 0 && i < items_len {
i
} else if i >= items_len {
items_len - 1
} else {
0
}
});
self.state.select(Some(selected_row));
}
}
pub fn current_selection(&self) -> &T {
&self.items[self.state.selected().unwrap_or(0)]
}
}
+111
View File
@@ -0,0 +1,111 @@
#[cfg(test)]
mod tests {
use crate::models::stateful_list::StatefulList;
use crate::models::Scrollable;
use pretty_assertions::{assert_eq, assert_str_eq};
#[test]
fn test_stateful_list_scrolling_on_empty_list_performs_no_op() {
let mut stateful_list: StatefulList<String> = StatefulList::default();
assert_eq!(stateful_list.state.selected(), None);
stateful_list.scroll_up();
assert_eq!(stateful_list.state.selected(), None);
stateful_list.scroll_down();
assert_eq!(stateful_list.state.selected(), None);
stateful_list.scroll_to_top();
assert_eq!(stateful_list.state.selected(), None);
stateful_list.scroll_to_bottom();
}
#[test]
fn test_stateful_list_scroll() {
let mut stateful_list = create_test_stateful_list();
assert_eq!(stateful_list.state.selected(), Some(0));
stateful_list.scroll_down();
assert_eq!(stateful_list.state.selected(), Some(1));
stateful_list.scroll_down();
assert_eq!(stateful_list.state.selected(), Some(0));
stateful_list.scroll_up();
assert_eq!(stateful_list.state.selected(), Some(1));
stateful_list.scroll_up();
assert_eq!(stateful_list.state.selected(), Some(0));
stateful_list.scroll_to_bottom();
assert_eq!(stateful_list.state.selected(), Some(1));
stateful_list.scroll_to_top();
assert_eq!(stateful_list.state.selected(), Some(0));
}
#[test]
fn test_stateful_list_set_items() {
let items_vec = vec!["Test 1", "Test 2", "Test 3"];
let mut stateful_list: StatefulList<&str> = StatefulList::default();
stateful_list.set_items(items_vec.clone());
assert_eq!(stateful_list.state.selected(), Some(0));
stateful_list.state.select(Some(1));
stateful_list.set_items(items_vec.clone());
assert_eq!(stateful_list.state.selected(), Some(1));
stateful_list.state.select(Some(3));
stateful_list.set_items(items_vec);
assert_eq!(stateful_list.state.selected(), Some(2));
}
#[test]
fn test_stateful_list_current_selection() {
let mut stateful_list = create_test_stateful_list();
assert_str_eq!(stateful_list.current_selection(), &stateful_list.items[0]);
stateful_list.state.select(Some(1));
assert_str_eq!(stateful_list.current_selection(), &stateful_list.items[1]);
}
#[test]
fn test_stateful_list_scroll_up() {
let mut stateful_list = create_test_stateful_list();
assert_eq!(stateful_list.state.selected(), Some(0));
stateful_list.scroll_up();
assert_eq!(stateful_list.state.selected(), Some(1));
stateful_list.scroll_up();
assert_eq!(stateful_list.state.selected(), Some(0));
}
fn create_test_stateful_list() -> StatefulList<&'static str> {
let mut stateful_list = StatefulList::default();
stateful_list.set_items(vec!["Test 1", "Test 2"]);
stateful_list
}
}
+299
View File
@@ -0,0 +1,299 @@
use crate::models::stateful_list::StatefulList;
use crate::models::{strip_non_search_characters, HorizontallyScrollableText, Scrollable};
use ratatui::widgets::TableState;
use std::cmp::Ordering;
use std::fmt::Debug;
#[cfg(test)]
#[path = "stateful_table_tests.rs"]
mod stateful_table_tests;
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct SortOption<T>
where
T: Clone + PartialEq + Eq + Debug,
{
pub name: &'static str,
pub cmp_fn: Option<fn(&T, &T) -> Ordering>,
}
#[derive(Default)]
pub struct StatefulTable<T>
where
T: Clone + PartialEq + Eq + Debug,
{
pub state: TableState,
pub items: Vec<T>,
pub filter: Option<HorizontallyScrollableText>,
pub search: Option<HorizontallyScrollableText>,
pub filtered_items: Option<Vec<T>>,
pub filtered_state: Option<TableState>,
pub sort_asc: bool,
pub sort: Option<StatefulList<SortOption<T>>>,
}
impl<T> Scrollable for StatefulTable<T>
where
T: Clone + PartialEq + Eq + Debug,
{
fn scroll_down(&mut self) {
if let Some(filtered_items) = self.filtered_items.as_ref() {
if filtered_items.is_empty() {
return;
}
let selected_row = match self.filtered_state.as_ref().unwrap().selected() {
Some(i) => {
if i >= filtered_items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self
.filtered_state
.as_mut()
.unwrap()
.select(Some(selected_row));
return;
}
if self.items.is_empty() {
return;
}
let selected_row = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(selected_row));
}
fn scroll_up(&mut self) {
if let Some(filtered_items) = self.filtered_items.as_ref() {
if filtered_items.is_empty() {
return;
}
let selected_row = match self.filtered_state.as_ref().unwrap().selected() {
Some(i) => {
if i == 0 {
filtered_items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self
.filtered_state
.as_mut()
.unwrap()
.select(Some(selected_row));
return;
}
if self.items.is_empty() {
return;
}
let selected_row = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(selected_row));
}
fn scroll_to_top(&mut self) {
if let Some(filtered_items) = self.filtered_items.as_ref() {
if filtered_items.is_empty() {
return;
}
self.filtered_state.as_mut().unwrap().select(Some(0));
return;
}
if self.items.is_empty() {
return;
}
self.state.select(Some(0));
}
fn scroll_to_bottom(&mut self) {
if let Some(filtered_items) = self.filtered_items.as_ref() {
if filtered_items.is_empty() {
return;
}
self
.filtered_state
.as_mut()
.unwrap()
.select(Some(filtered_items.len() - 1));
return;
}
if self.items.is_empty() {
return;
}
self.state.select(Some(self.items.len() - 1));
}
}
impl<T> StatefulTable<T>
where
T: Clone + PartialEq + Eq + Debug + Default,
{
pub fn set_items(&mut self, items: Vec<T>) {
let items_len = items.len();
self.items = items;
if !self.items.is_empty() {
let selected_row = self.state.selected().map_or(0, |i| {
if i > 0 && i < items_len {
i
} else if i >= items_len {
items_len - 1
} else {
0
}
});
self.state.select(Some(selected_row));
}
}
pub fn set_filtered_items(&mut self, filtered_items: Vec<T>) {
self.filtered_items = Some(filtered_items);
let mut filtered_state: TableState = Default::default();
filtered_state.select(Some(0));
self.filtered_state = Some(filtered_state);
}
pub fn select_index(&mut self, index: Option<usize>) {
if let Some(filtered_state) = &mut self.filtered_state {
filtered_state.select(index);
} else {
self.state.select(index);
}
}
pub fn current_selection(&self) -> &T {
if let Some(filtered_items) = &self.filtered_items {
&filtered_items[self
.filtered_state
.as_ref()
.unwrap()
.selected()
.unwrap_or(0)]
} else {
&self.items[self.state.selected().unwrap_or(0)]
}
}
pub fn sorting(&mut self, sort_options: Vec<SortOption<T>>) {
let mut sort_options_list = StatefulList::default();
sort_options_list.set_items(sort_options);
self.sort = Some(sort_options_list);
}
pub fn apply_sorting(&mut self) {
if let Some(sort_options) = &mut self.sort {
self.sort_asc = !self.sort_asc;
let selected_sort_option = sort_options.current_selection();
let mut items = self.filtered_items.as_ref().unwrap_or(&self.items).clone();
if let Some(cmp_fn) = selected_sort_option.cmp_fn {
if !self.sort_asc {
items.sort_by(|a, b| cmp_fn(a, b).reverse());
} else {
items.sort_by(cmp_fn);
}
if self.filtered_items.is_some() {
self.set_filtered_items(items.clone());
} else {
self.set_items(items);
}
}
}
}
pub fn apply_filter(&mut self, filter_field: fn(&T) -> &str) -> bool {
let filter_matches = match self.filter {
Some(ref filter) if !filter.text.is_empty() => {
let scrubbed_filter = strip_non_search_characters(&filter.text.clone());
self
.items
.iter()
.filter(|item| strip_non_search_characters(filter_field(item)).contains(&scrubbed_filter))
.cloned()
.collect()
}
_ => Vec::new(),
};
self.filter = None;
if filter_matches.is_empty() {
return false;
}
self.set_filtered_items(filter_matches);
true
}
pub fn reset_filter(&mut self) {
self.filter = None;
self.filtered_items = None;
self.filtered_state = None;
}
pub fn apply_search(&mut self, search_field: fn(&T) -> &str) -> bool {
let search_index = if let Some(search) = self.search.as_ref() {
let search_string = search.text.clone().to_lowercase();
self
.filtered_items
.as_ref()
.unwrap_or(&self.items)
.iter()
.position(|item| strip_non_search_characters(search_field(item)).contains(&search_string))
} else {
None
};
self.search = None;
if search_index.is_none() {
return false;
}
self.select_index(search_index);
true
}
pub fn reset_search(&mut self) {
self.search = None;
}
}
+615
View File
@@ -0,0 +1,615 @@
#[cfg(test)]
mod tests {
use crate::models::stateful_table::{SortOption, StatefulTable};
use crate::models::Scrollable;
use pretty_assertions::{assert_eq, assert_str_eq};
use ratatui::widgets::TableState;
#[test]
fn test_stateful_table_scrolling_on_empty_table_performs_no_op() {
let mut stateful_table: StatefulTable<String> = StatefulTable::default();
assert_eq!(stateful_table.state.selected(), None);
stateful_table.scroll_up();
assert_eq!(stateful_table.state.selected(), None);
stateful_table.scroll_down();
assert_eq!(stateful_table.state.selected(), None);
stateful_table.scroll_to_top();
assert_eq!(stateful_table.state.selected(), None);
stateful_table.scroll_to_bottom();
}
#[test]
fn test_stateful_table_filtered_scrolling_on_empty_table_performs_no_op() {
let mut filtered_stateful_table: StatefulTable<String> = StatefulTable {
filtered_items: Some(Vec::new()),
filtered_state: Some(TableState::default()),
..StatefulTable::default()
};
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
filtered_stateful_table.scroll_up();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
filtered_stateful_table.scroll_down();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
filtered_stateful_table.scroll_to_top();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
filtered_stateful_table.scroll_to_bottom();
}
#[test]
fn test_stateful_table_scroll() {
let mut stateful_table = create_test_stateful_table();
assert_eq!(stateful_table.state.selected(), Some(0));
stateful_table.scroll_down();
assert_eq!(stateful_table.state.selected(), Some(1));
stateful_table.scroll_down();
assert_eq!(stateful_table.state.selected(), Some(0));
stateful_table.scroll_up();
assert_eq!(stateful_table.state.selected(), Some(1));
stateful_table.scroll_up();
assert_eq!(stateful_table.state.selected(), Some(0));
stateful_table.scroll_to_bottom();
assert_eq!(stateful_table.state.selected(), Some(1));
stateful_table.scroll_to_top();
assert_eq!(stateful_table.state.selected(), Some(0));
}
#[test]
fn test_stateful_table_filtered_items_scroll() {
let mut filtered_stateful_table = create_test_filtered_stateful_table();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_table.scroll_down();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_table.scroll_down();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_table.scroll_up();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_table.scroll_up();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_table.scroll_to_bottom();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_table.scroll_to_top();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
}
#[test]
fn test_stateful_table_set_items() {
let items_vec = vec!["Test 1", "Test 2", "Test 3"];
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_items(items_vec.clone());
assert_eq!(stateful_table.state.selected(), Some(0));
stateful_table.state.select(Some(1));
stateful_table.set_items(items_vec.clone());
assert_eq!(stateful_table.state.selected(), Some(1));
stateful_table.state.select(Some(3));
stateful_table.set_items(items_vec);
assert_eq!(stateful_table.state.selected(), Some(2));
}
#[test]
fn test_stateful_table_set_filtered_items() {
let filtered_items_vec = vec!["Test 1", "Test 2", "Test 3"];
let mut filtered_stateful_table: StatefulTable<&str> = StatefulTable::default();
filtered_stateful_table.set_filtered_items(filtered_items_vec.clone());
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
assert_eq!(
filtered_stateful_table.filtered_items,
Some(filtered_items_vec.clone())
);
}
#[test]
fn test_stateful_table_current_selection() {
let mut stateful_table = create_test_stateful_table();
assert_str_eq!(stateful_table.current_selection(), &stateful_table.items[0]);
stateful_table.state.select(Some(1));
assert_str_eq!(stateful_table.current_selection(), &stateful_table.items[1]);
}
#[test]
fn test_stateful_table_sorting() {
let sort_options: Vec<SortOption<String>> = vec![
SortOption {
name: "Test 1",
cmp_fn: None,
},
SortOption {
name: "Test 2",
cmp_fn: None,
},
];
let mut stateful_table: StatefulTable<String> = StatefulTable::default();
stateful_table.sorting(sort_options.clone());
assert_eq!(
stateful_table.sort.as_ref().unwrap().items,
sort_options.clone()
);
assert_eq!(
stateful_table.sort.as_ref().unwrap().current_selection(),
&sort_options[0]
);
}
#[test]
fn test_stateful_table_apply_sorting_no_op_no_sort_options() {
let mut stateful_table = create_test_stateful_table();
let expected_items = stateful_table.items.clone();
stateful_table.apply_sorting();
assert_eq!(stateful_table.items, expected_items);
assert!(!stateful_table.sort_asc);
}
#[test]
fn test_stateful_table_apply_sorting_no_op_no_cmp_fn() {
let mut stateful_table = create_test_stateful_table();
stateful_table.sorting(vec![SortOption {
name: "Test 1",
cmp_fn: None,
}]);
let expected_items = stateful_table.items.clone();
stateful_table.apply_sorting();
assert_eq!(stateful_table.items, expected_items);
assert!(stateful_table.sort_asc);
}
#[test]
fn test_filtered_stateful_table_apply_sorting_no_op_no_cmp_fn() {
let mut filtered_stateful_table = create_test_filtered_stateful_table();
filtered_stateful_table.sorting(vec![SortOption {
name: "Test 1",
cmp_fn: None,
}]);
let expected_items = filtered_stateful_table
.filtered_items
.as_ref()
.unwrap()
.clone();
filtered_stateful_table.apply_sorting();
assert_eq!(
*filtered_stateful_table.filtered_items.as_ref().unwrap(),
expected_items
);
assert!(filtered_stateful_table.sort_asc);
}
#[test]
fn test_stateful_table_apply_sorting() {
let mut stateful_table = create_test_stateful_table();
stateful_table.sorting(vec![SortOption {
name: "Test 1",
cmp_fn: Some(|a, b| a.cmp(b)),
}]);
let mut expected_items = stateful_table.items.clone();
expected_items.sort();
stateful_table.apply_sorting();
assert_eq!(stateful_table.items, expected_items);
assert!(stateful_table.sort_asc);
stateful_table.apply_sorting();
expected_items.reverse();
assert_eq!(stateful_table.items, expected_items);
assert!(!stateful_table.sort_asc);
}
#[test]
fn test_filtered_stateful_table_apply_sorting() {
let mut filtered_stateful_table = create_test_filtered_stateful_table();
filtered_stateful_table.sorting(vec![SortOption {
name: "Test 1",
cmp_fn: Some(|a, b| a.cmp(b)),
}]);
let mut expected_items = filtered_stateful_table
.filtered_items
.as_mut()
.unwrap()
.clone();
expected_items.sort();
filtered_stateful_table.apply_sorting();
assert_eq!(
*filtered_stateful_table.filtered_items.as_ref().unwrap(),
expected_items
);
assert!(filtered_stateful_table.sort_asc);
filtered_stateful_table.apply_sorting();
expected_items.reverse();
assert_eq!(
*filtered_stateful_table.filtered_items.as_ref().unwrap(),
expected_items
);
assert!(!filtered_stateful_table.sort_asc);
}
#[test]
fn test_filtered_stateful_table_current_selection() {
let mut filtered_stateful_table = create_test_filtered_stateful_table();
assert_str_eq!(
filtered_stateful_table.current_selection(),
&filtered_stateful_table.filtered_items.as_ref().unwrap()[0]
);
filtered_stateful_table
.filtered_state
.as_mut()
.unwrap()
.select(Some(1));
assert_str_eq!(
filtered_stateful_table.current_selection(),
&filtered_stateful_table.filtered_items.as_ref().unwrap()[1]
);
}
#[test]
fn test_stateful_table_select_index() {
let mut stateful_table = create_test_stateful_table();
assert_eq!(stateful_table.state.selected(), Some(0));
stateful_table.select_index(Some(1));
assert_eq!(stateful_table.state.selected(), Some(1));
stateful_table.select_index(None);
assert_eq!(stateful_table.state.selected(), None);
}
#[test]
fn test_filtered_stateful_table_select_index() {
let mut filtered_stateful_table = create_test_filtered_stateful_table();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_table.select_index(Some(1));
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_table.select_index(None);
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
None
);
}
#[test]
fn test_stateful_table_scroll_up() {
let mut stateful_table = create_test_stateful_table();
assert_eq!(stateful_table.state.selected(), Some(0));
stateful_table.scroll_up();
assert_eq!(stateful_table.state.selected(), Some(1));
stateful_table.scroll_up();
assert_eq!(stateful_table.state.selected(), Some(0));
}
#[test]
fn test_filtered_stateful_table_scroll_up() {
let mut filtered_stateful_table = create_test_filtered_stateful_table();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
filtered_stateful_table.scroll_up();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(1)
);
filtered_stateful_table.scroll_up();
assert_eq!(
filtered_stateful_table
.filtered_state
.as_ref()
.unwrap()
.selected(),
Some(0)
);
}
#[test]
fn test_stateful_table_apply_filter() {
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_items(vec!["this", "is", "a", "test"]);
stateful_table.filter = Some("i".into());
let expected_items = vec!["this", "is"];
let mut expected_state = TableState::default();
expected_state.select(Some(0));
let has_matches = stateful_table.apply_filter(|&item| item);
assert_eq!(stateful_table.filter, None);
assert_eq!(stateful_table.filtered_items, Some(expected_items));
assert_eq!(stateful_table.filtered_state, Some(expected_state));
assert!(has_matches);
}
#[test]
fn test_stateful_table_apply_filter_no_matches() {
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_items(vec!["this", "is", "a", "test"]);
stateful_table.filter = Some("z".into());
let has_matches = stateful_table.apply_filter(|&item| item);
assert_eq!(stateful_table.filter, None);
assert_eq!(stateful_table.filtered_items, None);
assert_eq!(stateful_table.filtered_state, None);
assert!(!has_matches);
}
#[test]
fn test_stateful_table_reset_filter() {
let mut stateful_table = create_test_filtered_stateful_table();
stateful_table.reset_filter();
assert_eq!(stateful_table.filter, None);
assert_eq!(stateful_table.filtered_items, None);
assert_eq!(stateful_table.filtered_state, None);
}
#[test]
fn test_stateful_table_apply_search() {
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_items(vec!["this", "is", "a", "test"]);
stateful_table.search = Some("test".into());
let mut expected_state = TableState::default();
expected_state.select(Some(3));
let has_match = stateful_table.apply_search(|&item| item);
assert_eq!(stateful_table.search, None);
assert_eq!(stateful_table.state, expected_state);
assert!(has_match);
}
#[test]
fn test_stateful_table_apply_search_no_match() {
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_items(vec!["this", "is", "a", "test"]);
stateful_table.search = Some("shi-mon-a!".into());
let has_match = stateful_table.apply_search(|&item| item);
assert_eq!(stateful_table.search, None);
assert!(!has_match);
}
#[test]
fn test_filtered_stateful_table_apply_search() {
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_filtered_items(vec!["this", "is", "a", "test"]);
stateful_table.search = Some("test".into());
let mut expected_state = TableState::default();
expected_state.select(Some(3));
let has_match = stateful_table.apply_search(|&item| item);
assert_eq!(stateful_table.search, None);
assert_eq!(stateful_table.filtered_state, Some(expected_state));
assert!(has_match);
}
#[test]
fn test_filtered_stateful_table_apply_search_no_match() {
let mut stateful_table: StatefulTable<&str> = StatefulTable::default();
stateful_table.set_filtered_items(vec!["this", "is", "a", "test"]);
stateful_table.search = Some("shi-mon-a!".into());
let mut expected_state = TableState::default();
expected_state.select(Some(0));
let has_match = stateful_table.apply_search(|&item| item);
assert_eq!(stateful_table.search, None);
assert_eq!(stateful_table.filtered_state, Some(expected_state));
assert!(!has_match);
}
#[test]
fn test_stateful_table_reset_search() {
let mut stateful_table = create_test_stateful_table();
stateful_table.search = Some("test".into());
stateful_table.reset_search();
assert_eq!(stateful_table.search, None);
}
fn create_test_stateful_table() -> StatefulTable<&'static str> {
let mut stateful_table = StatefulTable::default();
stateful_table.set_items(vec!["Test 1", "Test 2"]);
stateful_table
}
fn create_test_filtered_stateful_table() -> StatefulTable<&'static str> {
let mut stateful_table = StatefulTable::default();
stateful_table.set_filtered_items(vec!["Test 1", "Test 2"]);
stateful_table
}
}
+2 -1
View File
@@ -20,7 +20,8 @@ use crate::models::servarr_data::radarr::modals::{
MovieDetailsModal,
};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText, StatefulTable};
use crate::models::stateful_table::StatefulTable;
use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText};
use crate::network::{Network, NetworkEvent, RequestMethod, RequestProps};
use crate::utils::{convert_runtime, convert_to_gb};
+1 -1
View File
@@ -17,7 +17,7 @@ mod test {
Quality, QualityWrapper, Rating, RatingsList,
};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::{HorizontallyScrollableText, StatefulTable};
use crate::models::HorizontallyScrollableText;
use crate::App;
use super::super::*;
+5 -29
View File
@@ -10,11 +10,12 @@ use ratatui::widgets::{Clear, List, ListItem};
use ratatui::Frame;
use crate::app::App;
use crate::models::{HorizontallyScrollableText, Route, StatefulList, TabState};
use crate::models::stateful_list::StatefulList;
use crate::models::{HorizontallyScrollableText, Route, TabState};
use crate::ui::radarr_ui::RadarrUi;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{
background_block, borderless_block, centered_rect, layout_block, layout_block_top_border,
background_block, borderless_block, centered_rect, layout_block_top_border,
layout_paragraph_borderless, logo_block, title_block, title_block_centered,
};
use crate::ui::widgets::button::Button;
@@ -210,25 +211,14 @@ pub fn draw_large_popup_over_background_fn_with_ui<T: DrawUi>(
draw_popup_over_ui::<T>(f, app, area, background_fn, 75, 75);
}
pub fn draw_drop_down_popup(
f: &mut Frame<'_>,
app: &mut App<'_>,
area: Rect,
background_fn: impl Fn(&mut Frame<'_>, &mut App<'_>, Rect),
drop_down_fn: impl Fn(&mut Frame<'_>, &mut App<'_>, Rect),
) {
draw_popup_over(f, app, area, background_fn, drop_down_fn, 20, 30);
}
fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -> Rect {
f.render_widget(title_block(title), area);
let [header_area, content_area] = Layout::vertical([Constraint::Length(1), Constraint::Fill(0)])
.margin(1)
.areas(area);
let [tabs_area, help_area] = Layout::horizontal([Constraint::Min(25), Constraint::Min(35)])
.flex(Flex::SpaceBetween)
.areas(header_area);
let [tabs_area, help_area] =
Layout::horizontal([Constraint::Percentage(45), Constraint::Fill(0)]).areas(header_area);
let titles = tab_state
.tabs
@@ -361,20 +351,6 @@ pub fn draw_prompt_box_with_checkboxes(
f.render_widget(no_button, no_area);
}
pub fn draw_selectable_list<'a, T>(
f: &mut Frame<'_>,
area: Rect,
content: &'a mut StatefulList<T>,
item_mapper: impl Fn(&T) -> ListItem<'a>,
) {
let items: Vec<ListItem<'_>> = content.items.iter().map(item_mapper).collect();
let list = List::new(items)
.block(layout_block())
.highlight_style(Style::new().highlight());
f.render_stateful_widget(list, area, &mut content.state);
}
pub fn draw_list_box<'a, T>(
f: &mut Frame<'_>,
area: Rect,
@@ -16,9 +16,10 @@ use crate::ui::utils::{layout_paragraph_borderless, title_block_centered};
use crate::ui::widgets::button::Button;
use crate::ui::widgets::checkbox::Checkbox;
use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::popup::Popup;
use crate::ui::widgets::selectable_list::SelectableList;
use crate::ui::{
draw_drop_down_popup, draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over,
draw_popup, draw_selectable_list, DrawUi,
draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over, draw_popup, DrawUi,
};
#[cfg(test)]
@@ -41,22 +42,12 @@ impl DrawUi for EditCollectionUi {
let draw_edit_collection_prompt =
|f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| match active_radarr_block {
ActiveRadarrBlock::EditCollectionSelectMinimumAvailability => {
draw_drop_down_popup(
f,
app,
prompt_area,
draw_edit_collection_confirmation_prompt,
draw_edit_collection_select_minimum_availability_popup,
);
draw_edit_collection_confirmation_prompt(f, app, prompt_area);
draw_edit_collection_select_minimum_availability_popup(f, app);
}
ActiveRadarrBlock::EditCollectionSelectQualityProfile => {
draw_drop_down_popup(
f,
app,
prompt_area,
draw_edit_collection_confirmation_prompt,
draw_edit_collection_select_quality_profile_popup,
);
draw_edit_collection_confirmation_prompt(f, app, prompt_area);
draw_edit_collection_select_quality_profile_popup(f, app);
}
ActiveRadarrBlock::EditCollectionPrompt
| ActiveRadarrBlock::EditCollectionToggleMonitored
@@ -180,14 +171,8 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
f.render_widget(cancel_button, cancel_area);
}
fn draw_edit_collection_select_minimum_availability_popup(
f: &mut Frame<'_>,
app: &mut App<'_>,
area: Rect,
) {
draw_selectable_list(
f,
area,
fn draw_edit_collection_select_minimum_availability_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let min_availability_list = SelectableList::new(
&mut app
.data
.radarr_data
@@ -197,16 +182,16 @@ fn draw_edit_collection_select_minimum_availability_popup(
.minimum_availability_list,
|minimum_availability| ListItem::new(minimum_availability.to_display_str().to_owned()),
);
let popup = Popup::new(min_availability_list, 20, 30);
f.render_widget(popup, f.size());
}
fn draw_edit_collection_select_quality_profile_popup(
f: &mut Frame<'_>,
app: &mut App<'_>,
area: Rect,
) {
draw_selectable_list(
f,
area,
let quality_profile_list = SelectableList::new(
&mut app
.data
.radarr_data
@@ -216,4 +201,7 @@ fn draw_edit_collection_select_quality_profile_popup(
.quality_profile_list,
|quality_profile| ListItem::new(quality_profile.clone()),
);
let popup = Popup::new(quality_profile_list, 20, 30);
f.render_widget(popup, f.size());
}
+30 -51
View File
@@ -21,9 +21,8 @@ use crate::ui::widgets::error_message::ErrorMessage;
use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::Popup;
use crate::ui::{
draw_drop_down_popup, draw_large_popup_over, draw_medium_popup_over, draw_selectable_list, DrawUi,
};
use crate::ui::widgets::selectable_list::SelectableList;
use crate::ui::{draw_large_popup_over, draw_medium_popup_over, DrawUi};
use crate::utils::convert_runtime;
use crate::{render_selectable_input_box, App};
@@ -266,40 +265,20 @@ fn draw_confirmation_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
match active_radarr_block {
ActiveRadarrBlock::AddMovieSelectMonitor => {
draw_drop_down_popup(
f,
app,
area,
draw_confirmation_prompt,
draw_add_movie_select_monitor_popup,
);
draw_confirmation_prompt(f, app, area);
draw_add_movie_select_monitor_popup(f, app);
}
ActiveRadarrBlock::AddMovieSelectMinimumAvailability => {
draw_drop_down_popup(
f,
app,
area,
draw_confirmation_prompt,
draw_add_movie_select_minimum_availability_popup,
);
draw_confirmation_prompt(f, app, area);
draw_add_movie_select_minimum_availability_popup(f, app);
}
ActiveRadarrBlock::AddMovieSelectQualityProfile => {
draw_drop_down_popup(
f,
app,
area,
draw_confirmation_prompt,
draw_add_movie_select_quality_profile_popup,
);
draw_confirmation_prompt(f, app, area);
draw_add_movie_select_quality_profile_popup(f, app);
}
ActiveRadarrBlock::AddMovieSelectRootFolder => {
draw_drop_down_popup(
f,
app,
area,
draw_confirmation_prompt,
draw_add_movie_select_root_folder_popup,
);
draw_confirmation_prompt(f, app, area);
draw_add_movie_select_root_folder_popup(f, app);
}
ActiveRadarrBlock::AddMoviePrompt | ActiveRadarrBlock::AddMovieTagsInput => {
draw_confirmation_prompt(f, app, area)
@@ -437,10 +416,8 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(cancel_button, cancel_area);
}
fn draw_add_movie_select_monitor_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
draw_selectable_list(
f,
area,
fn draw_add_movie_select_monitor_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let monitor_list = SelectableList::new(
&mut app
.data
.radarr_data
@@ -450,16 +427,13 @@ fn draw_add_movie_select_monitor_popup(f: &mut Frame<'_>, app: &mut App<'_>, are
.monitor_list,
|monitor| ListItem::new(monitor.to_display_str().to_owned()),
);
let popup = Popup::new(monitor_list, 20, 30);
f.render_widget(popup, f.size());
}
fn draw_add_movie_select_minimum_availability_popup(
f: &mut Frame<'_>,
app: &mut App<'_>,
area: Rect,
) {
draw_selectable_list(
f,
area,
fn draw_add_movie_select_minimum_availability_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let minimum_availability_list = SelectableList::new(
&mut app
.data
.radarr_data
@@ -469,12 +443,13 @@ fn draw_add_movie_select_minimum_availability_popup(
.minimum_availability_list,
|minimum_availability| ListItem::new(minimum_availability.to_display_str().to_owned()),
);
let popup = Popup::new(minimum_availability_list, 20, 30);
f.render_widget(popup, f.size());
}
fn draw_add_movie_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
draw_selectable_list(
f,
area,
fn draw_add_movie_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let quality_profile_list = SelectableList::new(
&mut app
.data
.radarr_data
@@ -484,12 +459,13 @@ fn draw_add_movie_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<
.quality_profile_list,
|quality_profile| ListItem::new(quality_profile.clone()),
);
let popup = Popup::new(quality_profile_list, 20, 30);
f.render_widget(popup, f.size());
}
fn draw_add_movie_select_root_folder_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
draw_selectable_list(
f,
area,
fn draw_add_movie_select_root_folder_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let root_folder_list = SelectableList::new(
&mut app
.data
.radarr_data
@@ -499,4 +475,7 @@ fn draw_add_movie_select_root_folder_popup(f: &mut Frame<'_>, app: &mut App<'_>,
.root_folder_list,
|root_folder| ListItem::new(root_folder.path.to_owned()),
);
let popup = Popup::new(root_folder_list, 20, 30);
f.render_widget(popup, f.size());
}
+17 -28
View File
@@ -18,9 +18,10 @@ use crate::ui::utils::{layout_paragraph_borderless, title_block_centered};
use crate::ui::widgets::button::Button;
use crate::ui::widgets::checkbox::Checkbox;
use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::popup::Popup;
use crate::ui::widgets::selectable_list::SelectableList;
use crate::ui::{
draw_drop_down_popup, draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over,
draw_popup, draw_selectable_list, DrawUi,
draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over, draw_popup, DrawUi,
};
#[cfg(test)]
@@ -43,22 +44,12 @@ impl DrawUi for EditMovieUi {
let draw_edit_movie_prompt =
|f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| match active_radarr_block {
ActiveRadarrBlock::EditMovieSelectMinimumAvailability => {
draw_drop_down_popup(
f,
app,
prompt_area,
draw_edit_movie_confirmation_prompt,
draw_edit_movie_select_minimum_availability_popup,
);
draw_edit_movie_confirmation_prompt(f, app, prompt_area);
draw_edit_movie_select_minimum_availability_popup(f, app);
}
ActiveRadarrBlock::EditMovieSelectQualityProfile => {
draw_drop_down_popup(
f,
app,
prompt_area,
draw_edit_movie_confirmation_prompt,
draw_edit_movie_select_quality_profile_popup,
);
draw_edit_movie_confirmation_prompt(f, app, prompt_area);
draw_edit_movie_select_quality_profile_popup(f, app);
}
ActiveRadarrBlock::EditMoviePrompt
| ActiveRadarrBlock::EditMovieToggleMonitored
@@ -190,14 +181,8 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
f.render_widget(cancel_button, cancel_area);
}
fn draw_edit_movie_select_minimum_availability_popup(
f: &mut Frame<'_>,
app: &mut App<'_>,
area: Rect,
) {
draw_selectable_list(
f,
area,
fn draw_edit_movie_select_minimum_availability_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let minimum_availability_list = SelectableList::new(
&mut app
.data
.radarr_data
@@ -207,12 +192,13 @@ fn draw_edit_movie_select_minimum_availability_popup(
.minimum_availability_list,
|minimum_availability| ListItem::new(minimum_availability.to_display_str().to_owned()),
);
let popup = Popup::new(minimum_availability_list, 20, 30);
f.render_widget(popup, f.size());
}
fn draw_edit_movie_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
draw_selectable_list(
f,
area,
fn draw_edit_movie_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let quality_profile_list = SelectableList::new(
&mut app
.data
.radarr_data
@@ -222,4 +208,7 @@ fn draw_edit_movie_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App
.quality_profile_list,
|quality_profile| ListItem::new(quality_profile.clone()),
);
let popup = Popup::new(quality_profile_list, 20, 30);
f.render_widget(popup, f.size());
}
+93 -145
View File
@@ -3,11 +3,11 @@ use std::iter;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Cell, ListItem, Paragraph, Row, Wrap};
use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
use ratatui::Frame;
use crate::app::App;
use crate::models::radarr_models::{Credit, MovieHistoryItem, Release, ReleaseField};
use crate::models::radarr_models::{Credit, MovieHistoryItem, Release};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS};
use crate::models::Route;
use crate::ui::radarr_ui::library::draw_library;
@@ -18,8 +18,8 @@ use crate::ui::utils::{
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::{
draw_drop_down_popup, draw_large_popup_over, draw_prompt_box, draw_prompt_box_with_content,
draw_prompt_popup_over, draw_selectable_list, draw_small_popup_over, draw_tabs, DrawUi,
draw_large_popup_over, draw_prompt_box, draw_prompt_box_with_content, draw_prompt_popup_over,
draw_small_popup_over, draw_tabs, DrawUi,
};
use crate::utils::convert_to_gb;
@@ -63,26 +63,6 @@ impl DrawUi for MovieDetailsUi {
draw_movie_info,
draw_update_and_scan_prompt,
),
ActiveRadarrBlock::ManualSearchSortPrompt => draw_drop_down_popup(
f,
app,
content_area,
draw_movie_info,
|f, app, content_area| {
draw_selectable_list(
f,
content_area,
&mut app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap()
.movie_releases_sort,
|sort_option| ListItem::new(sort_option.to_string()),
)
},
),
ActiveRadarrBlock::ManualSearchConfirmPrompt => draw_small_popup_over(
f,
app,
@@ -388,142 +368,110 @@ fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
}
fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let (current_selection, is_empty, sort_ascending) =
match app.data.radarr_data.movie_details_modal.as_ref() {
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
let (current_selection, is_empty) = match app.data.radarr_data.movie_details_modal.as_ref() {
Some(movie_details_modal) if !movie_details_modal.movie_releases.items.is_empty() => (
movie_details_modal
.movie_releases
.current_selection()
.clone(),
movie_details_modal.movie_releases.items.is_empty(),
movie_details_modal.sort_ascending,
),
_ => (Release::default(), true, None),
_ => (Release::default(), true),
};
let current_route = *app.get_current_route();
let help_footer = app
.data
.radarr_data
.movie_info_tabs
.get_active_tab_contextual_help();
let mut table_headers_vec = vec![
"Source".to_owned(),
"Age".to_owned(),
"".to_owned(),
"Title".to_owned(),
"Indexer".to_owned(),
"Size".to_owned(),
"Peers".to_owned(),
"Language".to_owned(),
"Quality".to_owned(),
];
if let Some(ascending) = sort_ascending {
let direction = if ascending { "" } else { "" };
match app
let current_route = *app.get_current_route();
let help_footer = app
.data
.radarr_data
.movie_details_modal
.as_ref()
.unwrap()
.movie_releases_sort
.current_selection()
{
ReleaseField::Source => table_headers_vec[0].push_str(direction),
ReleaseField::Age => table_headers_vec[1].push_str(direction),
ReleaseField::Rejected => table_headers_vec[2].push_str(direction),
ReleaseField::Title => table_headers_vec[3].push_str(direction),
ReleaseField::Indexer => table_headers_vec[4].push_str(direction),
ReleaseField::Size => table_headers_vec[5].push_str(direction),
ReleaseField::Peers => table_headers_vec[6].push_str(direction),
ReleaseField::Language => table_headers_vec[7].push_str(direction),
ReleaseField::Quality => table_headers_vec[8].push_str(direction),
}
}
let content = Some(
&mut app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap()
.movie_releases,
);
let releases_row_mapping = |release: &Release| {
let Release {
protocol,
age,
title,
indexer,
size,
rejected,
seeders,
leechers,
languages,
quality,
..
} = release;
let age = format!("{age} days");
title.scroll_left_or_reset(
get_width_from_percentage(area, 30),
current_selection == *release
&& current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(),
app.tick_count % app.ticks_until_scroll == 0,
.movie_info_tabs
.get_active_tab_contextual_help();
let content = Some(
&mut app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap()
.movie_releases,
);
let size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" };
let peers = if seeders.is_none() || leechers.is_none() {
Text::from("")
} else {
let seeders = seeders.clone().unwrap().as_u64().unwrap();
let leechers = leechers.clone().unwrap().as_u64().unwrap();
decorate_peer_style(
let releases_row_mapping = |release: &Release| {
let Release {
protocol,
age,
title,
indexer,
size,
rejected,
seeders,
leechers,
Text::from(format!("{seeders} / {leechers}")),
)
languages,
quality,
..
} = release;
let age = format!("{age} days");
title.scroll_left_or_reset(
get_width_from_percentage(area, 30),
current_selection == *release
&& current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(),
app.tick_count % app.ticks_until_scroll == 0,
);
let size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" };
let peers = if seeders.is_none() || leechers.is_none() {
Text::from("")
} else {
let seeders = seeders.clone().unwrap().as_u64().unwrap();
let leechers = leechers.clone().unwrap().as_u64().unwrap();
decorate_peer_style(
seeders,
leechers,
Text::from(format!("{seeders} / {leechers}")),
)
};
let language = if languages.is_some() {
languages.clone().unwrap()[0].name.clone()
} else {
String::new()
};
let quality = quality.quality.name.clone();
Row::new(vec![
Cell::from(protocol.clone()),
Cell::from(age),
Cell::from(rejected_str),
Cell::from(title.to_string()),
Cell::from(indexer.clone()),
Cell::from(format!("{size:.1} GB")),
Cell::from(peers),
Cell::from(language),
Cell::from(quality),
])
.primary()
};
let releases_table = ManagarrTable::new(content, releases_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading || is_empty)
.footer(help_footer)
.sorting(active_radarr_block == ActiveRadarrBlock::ManualSearchSortPrompt)
.headers([
"Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality",
])
.constraints([
Constraint::Length(9),
Constraint::Length(10),
Constraint::Length(5),
Constraint::Percentage(30),
Constraint::Percentage(18),
Constraint::Length(12),
Constraint::Length(12),
Constraint::Percentage(7),
Constraint::Percentage(10),
]);
let language = if languages.is_some() {
languages.clone().unwrap()[0].name.clone()
} else {
String::new()
};
let quality = quality.quality.name.clone();
Row::new(vec![
Cell::from(protocol.clone()),
Cell::from(age),
Cell::from(rejected_str),
Cell::from(title.to_string()),
Cell::from(indexer.clone()),
Cell::from(format!("{size:.1} GB")),
Cell::from(peers),
Cell::from(language),
Cell::from(quality),
])
.primary()
};
let releases_table = ManagarrTable::new(content, releases_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading || is_empty)
.footer(help_footer)
.headers(table_headers_vec.iter().map(|s| &**s))
.constraints([
Constraint::Length(9),
Constraint::Length(10),
Constraint::Length(5),
Constraint::Percentage(30),
Constraint::Percentage(18),
Constraint::Length(12),
Constraint::Length(12),
Constraint::Percentage(7),
Constraint::Percentage(10),
]);
f.render_widget(releases_table, area);
f.render_widget(releases_table, area);
}
}
fn draw_manual_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
+48 -8
View File
@@ -1,19 +1,23 @@
use crate::models::StatefulTable;
use crate::models::stateful_table::StatefulTable;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::layout_block_top_border;
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::popup::Popup;
use crate::ui::widgets::selectable_list::SelectableList;
use crate::ui::HIGHLIGHT_SYMBOL;
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::prelude::{Style, Stylize, Text};
use ratatui::widgets::{Block, Paragraph, Row, StatefulWidget, Table, Widget};
use ratatui::widgets::{Block, ListItem, Paragraph, Row, StatefulWidget, Table, Widget};
use std::fmt::Debug;
pub struct ManagarrTable<'a, T, F>
where
F: Fn(&T) -> Row<'a>,
T: Clone + PartialEq + Eq + Debug,
{
content: Option<&'a mut StatefulTable<T>>,
table_headers: Vec<Text<'a>>,
table_headers: Vec<String>,
constraints: Vec<Constraint>,
row_mapper: F,
footer: Option<String>,
@@ -22,11 +26,13 @@ where
margin: u16,
is_loading: bool,
highlight_rows: bool,
is_sorting: bool,
}
impl<'a, T, F> ManagarrTable<'a, T, F>
where
F: Fn(&T) -> Row<'a>,
T: Clone + PartialEq + Eq + Debug,
{
pub fn new(content: Option<&'a mut StatefulTable<T>>, row_mapper: F) -> Self {
Self {
@@ -40,13 +46,14 @@ where
margin: 0,
is_loading: false,
highlight_rows: true,
is_sorting: false,
}
}
pub fn headers<I>(mut self, headers: I) -> Self
where
I: IntoIterator,
I::Item: Into<Text<'a>>,
I::Item: Into<String>,
{
self.table_headers = headers.into_iter().map(Into::into).collect();
self
@@ -91,7 +98,13 @@ where
self
}
pub fn sorting(mut self, is_sorting: bool) -> Self {
self.is_sorting = is_sorting;
self
}
fn render_table(self, area: Rect, buf: &mut Buffer) {
let table_headers = self.parse_headers();
let table_area = if let Some(ref footer) = self.footer {
let [content_area, footer_area] =
Layout::vertical([Constraint::Fill(0), Constraint::Length(2)])
@@ -121,10 +134,7 @@ where
if !table_contents.is_empty() {
let rows = table_contents.iter().map(&self.row_mapper);
let headers = Row::new(self.table_headers)
.default()
.bold()
.bottom_margin(0);
let headers = Row::new(table_headers).default().bold().bottom_margin(0);
let mut table = Table::new(rows, &self.constraints)
.header(headers)
@@ -137,6 +147,13 @@ where
}
StatefulWidget::render(table, table_area, buf, table_state);
if content.sort.is_some() && self.is_sorting {
let selectable_list = SelectableList::new(content.sort.as_mut().unwrap(), |item| {
ListItem::new(Text::from(item.name))
});
Popup::new(selectable_list, 20, 50).render(table_area, buf);
}
} else {
loading_block.render(table_area, buf);
}
@@ -144,11 +161,34 @@ where
loading_block.render(table_area, buf);
}
}
fn parse_headers(&self) -> Vec<Text<'a>> {
if let Some(ref content) = self.content {
if let Some(ref sort_list) = content.sort {
if !self.is_sorting {
let mut new_headers = self.table_headers.clone();
let idx = sort_list.state.selected().unwrap_or(0);
let direction = if content.sort_asc { "" } else { "" };
new_headers[idx].push_str(direction);
return new_headers.into_iter().map(Text::from).collect();
}
}
}
self
.table_headers
.clone()
.into_iter()
.map(Text::from)
.collect()
}
}
impl<'a, T, F> Widget for ManagarrTable<'a, T, F>
where
F: Fn(&T) -> Row<'a>,
T: Clone + PartialEq + Eq + Debug,
{
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_table(area, buf);
+1
View File
@@ -5,3 +5,4 @@ pub(super) mod input_box;
pub(super) mod loading_block;
pub(super) mod managarr_table;
pub(super) mod popup;
pub(super) mod selectable_list;
+47
View File
@@ -0,0 +1,47 @@
use crate::models::stateful_list::StatefulList;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::layout_block;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::Widget;
use ratatui::style::Style;
use ratatui::widgets::{List, ListItem, StatefulWidget};
pub struct SelectableList<'a, T, F>
where
F: Fn(&T) -> ListItem<'a>,
{
content: &'a mut StatefulList<T>,
row_mapper: F,
}
impl<'a, T, F> SelectableList<'a, T, F>
where
F: Fn(&T) -> ListItem<'a>,
{
pub fn new(content: &'a mut StatefulList<T>, row_mapper: F) -> Self {
Self {
content,
row_mapper,
}
}
fn render_list(self, area: Rect, buf: &mut Buffer) {
let items: Vec<ListItem<'_>> = self.content.items.iter().map(&self.row_mapper).collect();
let selectable_list = List::new(items)
.block(layout_block())
.highlight_style(Style::new().highlight());
StatefulWidget::render(selectable_list, area, buf, &mut self.content.state);
}
}
impl<'a, T, F> Widget for SelectableList<'a, T, F>
where
F: Fn(&T) -> ListItem<'a>,
{
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_list(area, buf);
}
}