From b49bfaa9c14bc2def4dbf60c34c40048dc6c8fe7 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 14 Feb 2024 16:09:42 -0700 Subject: [PATCH] Added sorting to the collections table, and fixed a bug that was refreshing the underlying table while users may be selecting a sort option. --- Cargo.lock | 6 +- Cargo.toml | 2 +- README.md | 2 +- src/app/radarr/radarr_context_clues.rs | 5 +- src/app/radarr/radarr_context_clues_tests.rs | 15 +- .../collections/collections_handler_tests.rs | 331 +++++++++++++++++- .../radarr_handlers/collections/mod.rs | 123 ++++++- src/models/servarr_data/radarr/radarr_data.rs | 4 +- .../servarr_data/radarr/radarr_data_tests.rs | 3 +- src/network/radarr_network.rs | 22 +- src/network/radarr_network_tests.rs | 278 +++++++++++++-- src/ui/radarr_ui/collections/mod.rs | 131 +++---- 12 files changed, 814 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a99152..6e6b6f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -863,7 +863,7 @@ dependencies = [ [[package]] name = "managarr" -version = "0.0.32" +version = "0.0.33" dependencies = [ "anyhow", "backtrace", @@ -1131,9 +1131,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "powerfmt" diff --git a/Cargo.toml b/Cargo.toml index 92b79ff..c6d588d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managarr" -version = "0.0.32" +version = "0.0.33" authors = ["Alex Clarke "] description = "A TUI to manage your Servarrs" keywords = ["managarr", "tui-rs", "dashboard", "servarr", "tui"] diff --git a/README.md b/README.md index cadc1d8..e20b5d2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # managarr - A TUI to manage your Servarrs -Managarr is a TUI to help you manage your HTPC (Home Theater PC). Built with love in Rust! +Managarr is a TUI to help you manage your HTPC (Home Theater PC). Built with 🤎 in Rust! ![library](screenshots/library.png) diff --git a/src/app/radarr/radarr_context_clues.rs b/src/app/radarr/radarr_context_clues.rs index 541aa59..e4cae95 100644 --- a/src/app/radarr/radarr_context_clues.rs +++ b/src/app/radarr/radarr_context_clues.rs @@ -8,10 +8,10 @@ mod radarr_context_clues_tests; pub static LIBRARY_CONTEXT_CLUES: [ContextClue; 10] = [ (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), - (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), ( DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh.desc, @@ -29,9 +29,10 @@ pub static DOWNLOADS_CONTEXT_CLUES: [ContextClue; 2] = [ (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), ]; -pub static COLLECTIONS_CONTEXT_CLUES: [ContextClue; 7] = [ +pub static COLLECTIONS_CONTEXT_CLUES: [ContextClue; 8] = [ (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), ( DEFAULT_KEYBINDINGS.refresh, diff --git a/src/app/radarr/radarr_context_clues_tests.rs b/src/app/radarr/radarr_context_clues_tests.rs index 07cc68d..73a1893 100644 --- a/src/app/radarr/radarr_context_clues_tests.rs +++ b/src/app/radarr/radarr_context_clues_tests.rs @@ -27,6 +27,11 @@ mod tests { let (key_binding, description) = library_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); + + let (key_binding, description) = library_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); @@ -42,11 +47,6 @@ mod tests { let (key_binding, description) = library_context_clues_iter.next().unwrap(); - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); - - let (key_binding, description) = library_context_clues_iter.next().unwrap(); - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); @@ -99,6 +99,11 @@ mod tests { let (key_binding, description) = collections_context_clues.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); + + let (key_binding, description) = collections_context_clues.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc); diff --git a/src/handlers/radarr_handlers/collections/collections_handler_tests.rs b/src/handlers/radarr_handlers/collections/collections_handler_tests.rs index fbbea91..03af63c 100644 --- a/src/handlers/radarr_handlers/collections/collections_handler_tests.rs +++ b/src/handlers/radarr_handlers/collections/collections_handler_tests.rs @@ -2,17 +2,22 @@ mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; + use std::cmp::Ordering; + use std::iter; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; - use crate::handlers::radarr_handlers::collections::CollectionsHandler; + use crate::handlers::radarr_handlers::collections::{ + collections_sorting_options, CollectionsHandler, + }; use crate::handlers::KeyEventHandler; - use crate::models::radarr_models::Collection; + use crate::models::radarr_models::{Collection, CollectionMovie}; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, COLLECTIONS_BLOCKS, COLLECTION_DETAILS_BLOCKS, EDIT_COLLECTION_BLOCKS, }; + use crate::models::stateful_table::SortOption; use crate::models::HorizontallyScrollableText; use crate::{extended_stateful_iterable_vec, test_handler_delegation}; @@ -20,6 +25,7 @@ mod tests { use rstest::rstest; use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; + use pretty_assertions::assert_eq; use super::*; @@ -33,6 +39,61 @@ mod tests { title, to_string ); + + #[rstest] + fn test_collections_sort_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let collection_field_vec = sort_options(); + let mut app = App::default(); + app.data.radarr_data.collections.sorting(sort_options()); + + if key == Key::Up { + for i in (0..collection_field_vec.len()).rev() { + CollectionsHandler::with( + &key, + &mut app, + &ActiveRadarrBlock::CollectionsSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .collections + .sort + .as_ref() + .unwrap() + .current_selection(), + &collection_field_vec[i] + ); + } + } else { + for i in 0..collection_field_vec.len() { + CollectionsHandler::with( + &key, + &mut app, + &ActiveRadarrBlock::CollectionsSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .collections + .sort + .as_ref() + .unwrap() + .current_selection(), + &collection_field_vec[(i + 1) % collection_field_vec.len()] + ); + } + } + } } mod test_handle_home_end { @@ -148,6 +209,53 @@ mod tests { 0 ); } + + #[test] + fn test_collections_sort_home_end() { + let collection_field_vec = sort_options(); + let mut app = App::default(); + app.data.radarr_data.collections.sorting(sort_options()); + + CollectionsHandler::with( + &DEFAULT_KEYBINDINGS.end.key, + &mut app, + &ActiveRadarrBlock::CollectionsSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .collections + .sort + .as_ref() + .unwrap() + .current_selection(), + &collection_field_vec[collection_field_vec.len() - 1] + ); + + CollectionsHandler::with( + &DEFAULT_KEYBINDINGS.home.key, + &mut app, + &ActiveRadarrBlock::CollectionsSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .collections + .sort + .as_ref() + .unwrap() + .current_selection(), + &collection_field_vec[0] + ); + } } mod test_handle_left_right_action { @@ -600,6 +708,38 @@ mod tests { &ActiveRadarrBlock::Collections.into() ); } + + #[test] + fn test_collections_sort_prompt_submit() { + let mut app = App::default(); + app.data.radarr_data.collections.sort_asc = true; + app.data.radarr_data.collections.sorting(sort_options()); + app + .data + .radarr_data + .collections + .set_items(collections_vec()); + app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); + app.push_navigation_stack(ActiveRadarrBlock::CollectionsSortPrompt.into()); + + let mut expected_vec = collections_vec(); + expected_vec.sort_by(|a, b| a.id.cmp(&b.id)); + expected_vec.reverse(); + + CollectionsHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::CollectionsSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Collections.into() + ); + assert_eq!(app.data.radarr_data.collections.items, expected_vec); + } } mod test_handle_esc { @@ -948,6 +1088,36 @@ mod tests { "h" ); } + + #[test] + fn test_sort_key() { + let mut app = App::default(); + + CollectionsHandler::with( + &DEFAULT_KEYBINDINGS.sort.key, + &mut app, + &ActiveRadarrBlock::Collections, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::CollectionsSortPrompt.into() + ); + assert_eq!( + app + .data + .radarr_data + .collections + .sort + .as_ref() + .unwrap() + .items, + collections_sorting_options() + ); + assert!(!app.data.radarr_data.collections.sort_asc); + } } #[rstest] @@ -982,6 +1152,163 @@ mod tests { ); } + #[test] + fn test_collections_sorting_options_collection() { + let expected_cmp_fn: fn(&Collection, &Collection) -> Ordering = |a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }; + let mut expected_collections_vec = collections_vec(); + expected_collections_vec.sort_by(expected_cmp_fn); + + let sort_option = collections_sorting_options()[0].clone(); + let mut sorted_collections_vec = collections_vec(); + sorted_collections_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_collections_vec, expected_collections_vec); + assert_str_eq!(sort_option.name, "Collection"); + } + + #[test] + fn test_collections_sorting_options_number_of_movies() { + let expected_cmp_fn: fn(&Collection, &Collection) -> Ordering = |a, b| { + let a_movie_count = a.movies.as_ref().unwrap_or(&Vec::new()).len(); + let b_movie_count = b.movies.as_ref().unwrap_or(&Vec::new()).len(); + + a_movie_count.cmp(&b_movie_count) + }; + let mut expected_collections_vec = collections_vec(); + expected_collections_vec.sort_by(expected_cmp_fn); + + let sort_option = collections_sorting_options()[1].clone(); + let mut sorted_collections_vec = collections_vec(); + sorted_collections_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_collections_vec, expected_collections_vec); + assert_str_eq!(sort_option.name, "Number of Movies"); + } + + #[test] + fn test_collections_sorting_options_root_folder_path() { + let expected_cmp_fn: fn(&Collection, &Collection) -> Ordering = |a, b| { + let a_root_folder = a + .root_folder_path + .as_ref() + .unwrap_or(&String::new()) + .to_owned(); + let b_root_folder = b + .root_folder_path + .as_ref() + .unwrap_or(&String::new()) + .to_owned(); + + a_root_folder.cmp(&b_root_folder) + }; + let mut expected_collections_vec = collections_vec(); + expected_collections_vec.sort_by(expected_cmp_fn); + + let sort_option = collections_sorting_options()[2].clone(); + let mut sorted_collections_vec = collections_vec(); + sorted_collections_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_collections_vec, expected_collections_vec); + assert_str_eq!(sort_option.name, "Root Folder Path"); + } + + #[test] + fn test_collections_sorting_options_quality_profile() { + let expected_cmp_fn: fn(&Collection, &Collection) -> Ordering = + |a, b| a.quality_profile_id.cmp(&b.quality_profile_id); + let mut expected_collections_vec = collections_vec(); + expected_collections_vec.sort_by(expected_cmp_fn); + + let sort_option = collections_sorting_options()[3].clone(); + let mut sorted_collections_vec = collections_vec(); + sorted_collections_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_collections_vec, expected_collections_vec); + assert_str_eq!(sort_option.name, "Quality Profile"); + } + + #[test] + fn test_collections_sorting_options_search_on_add() { + let expected_cmp_fn: fn(&Collection, &Collection) -> Ordering = + |a, b| a.search_on_add.cmp(&b.search_on_add); + let mut expected_collections_vec = collections_vec(); + expected_collections_vec.sort_by(expected_cmp_fn); + + let sort_option = collections_sorting_options()[4].clone(); + let mut sorted_collections_vec = collections_vec(); + sorted_collections_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_collections_vec, expected_collections_vec); + assert_str_eq!(sort_option.name, "Search on Add"); + } + + #[test] + fn test_collections_sorting_options_monitored() { + let expected_cmp_fn: fn(&Collection, &Collection) -> Ordering = + |a, b| a.monitored.cmp(&b.monitored); + let mut expected_collections_vec = collections_vec(); + expected_collections_vec.sort_by(expected_cmp_fn); + + let sort_option = collections_sorting_options()[5].clone(); + let mut sorted_collections_vec = collections_vec(); + sorted_collections_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_collections_vec, expected_collections_vec); + assert_str_eq!(sort_option.name, "Monitored"); + } + + fn collections_vec() -> Vec { + vec![ + Collection { + id: 3, + title: "test 1".into(), + movies: Some(iter::repeat(CollectionMovie::default()).take(3).collect()), + root_folder_path: Some("/nfs/movies".into()), + quality_profile_id: 1, + search_on_add: false, + monitored: true, + ..Collection::default() + }, + Collection { + id: 2, + title: "test 2".into(), + movies: Some(iter::repeat(CollectionMovie::default()).take(7).collect()), + root_folder_path: Some("/htpc/movies".into()), + quality_profile_id: 3, + search_on_add: true, + monitored: true, + ..Collection::default() + }, + Collection { + id: 1, + title: "test 3".into(), + movies: Some(iter::repeat(CollectionMovie::default()).take(1).collect()), + root_folder_path: Some("/nfs/some/stupidly/long/path/to/test/with".into()), + quality_profile_id: 1, + search_on_add: false, + monitored: false, + ..Collection::default() + }, + ] + } + + fn sort_options() -> Vec> { + vec![SortOption { + name: "Test 1", + cmp_fn: Some(|a, b| { + b.title + .text + .to_lowercase() + .cmp(&a.title.text.to_lowercase()) + }), + }] + } + #[test] fn test_collections_handler_accepts() { let mut collections_handler_blocks = Vec::new(); diff --git a/src/handlers/radarr_handlers/collections/mod.rs b/src/handlers/radarr_handlers/collections/mod.rs index 06c852c..a273700 100644 --- a/src/handlers/radarr_handlers/collections/mod.rs +++ b/src/handlers/radarr_handlers/collections/mod.rs @@ -5,9 +5,11 @@ use crate::handlers::radarr_handlers::collections::collection_details_handler::C use crate::handlers::radarr_handlers::collections::edit_collection_handler::EditCollectionHandler; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; +use crate::models::radarr_models::Collection; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, COLLECTIONS_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, }; +use crate::models::stateful_table::SortOption; use crate::models::{BlockSelectionState, HorizontallyScrollableText, Scrollable}; use crate::network::radarr_network::RadarrEvent; use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; @@ -66,14 +68,34 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' } fn handle_scroll_up(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Collections { - self.app.data.radarr_data.collections.scroll_up() + match self.active_radarr_block { + ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_up(), + ActiveRadarrBlock::CollectionsSortPrompt => self + .app + .data + .radarr_data + .collections + .sort + .as_mut() + .unwrap() + .scroll_up(), + _ => (), } } fn handle_scroll_down(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Collections { - self.app.data.radarr_data.collections.scroll_down() + match self.active_radarr_block { + ActiveRadarrBlock::Collections => self.app.data.radarr_data.collections.scroll_down(), + ActiveRadarrBlock::CollectionsSortPrompt => self + .app + .data + .radarr_data + .collections + .sort + .as_mut() + .unwrap() + .scroll_down(), + _ => (), } } @@ -98,6 +120,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' .as_mut() .unwrap() .scroll_home(), + ActiveRadarrBlock::CollectionsSortPrompt => self + .app + .data + .radarr_data + .collections + .sort + .as_mut() + .unwrap() + .scroll_to_top(), _ => (), } } @@ -123,6 +154,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' .as_mut() .unwrap() .reset_offset(), + ActiveRadarrBlock::CollectionsSortPrompt => self + .app + .data + .radarr_data + .collections + .sort + .as_mut() + .unwrap() + .scroll_to_bottom(), _ => (), } } @@ -215,6 +255,18 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' self.app.pop_navigation_stack(); } + ActiveRadarrBlock::CollectionsSortPrompt => { + self + .app + .data + .radarr_data + .collections + .items + .sort_by(|a, b| a.id.cmp(&b.id)); + self.app.data.radarr_data.collections.apply_sorting(); + + self.app.pop_navigation_stack(); + } _ => (), } } @@ -285,6 +337,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { self.app.should_refresh = true; } + _ if *key == DEFAULT_KEYBINDINGS.sort.key => { + self + .app + .data + .radarr_data + .collections + .sorting(collections_sorting_options()); + self + .app + .push_navigation_stack(ActiveRadarrBlock::CollectionsSortPrompt.into()); + } _ => (), }, ActiveRadarrBlock::SearchCollection => { @@ -319,3 +382,55 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' } } } + +fn collections_sorting_options() -> Vec> { + vec![ + SortOption { + name: "Collection", + cmp_fn: Some(|a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }), + }, + SortOption { + name: "Number of Movies", + cmp_fn: Some(|a, b| { + let a_movie_count = a.movies.as_ref().unwrap_or(&Vec::new()).len(); + let b_movie_count = b.movies.as_ref().unwrap_or(&Vec::new()).len(); + + a_movie_count.cmp(&b_movie_count) + }), + }, + SortOption { + name: "Root Folder Path", + cmp_fn: Some(|a, b| { + let a_root_folder = a + .root_folder_path + .as_ref() + .unwrap_or(&String::new()) + .to_owned(); + let b_root_folder = b + .root_folder_path + .as_ref() + .unwrap_or(&String::new()) + .to_owned(); + + a_root_folder.cmp(&b_root_folder) + }), + }, + SortOption { + name: "Quality Profile", + cmp_fn: Some(|a, b| a.quality_profile_id.cmp(&b.quality_profile_id)), + }, + SortOption { + name: "Search on Add", + cmp_fn: Some(|a, b| a.search_on_add.cmp(&b.search_on_add)), + }, + SortOption { + name: "Monitored", + cmp_fn: Some(|a, b| a.monitored.cmp(&b.monitored)), + }, + ] +} diff --git a/src/models/servarr_data/radarr/radarr_data.rs b/src/models/servarr_data/radarr/radarr_data.rs index e98d696..c14294f 100644 --- a/src/models/servarr_data/radarr/radarr_data.rs +++ b/src/models/servarr_data/radarr/radarr_data.rs @@ -212,6 +212,7 @@ pub enum ActiveRadarrBlock { AddRootFolderPrompt, AutomaticallySearchMoviePrompt, Collections, + CollectionsSortPrompt, CollectionDetails, Cast, Crew, @@ -299,8 +300,9 @@ pub static LIBRARY_BLOCKS: [ActiveRadarrBlock; 7] = [ ActiveRadarrBlock::FilterMoviesError, ActiveRadarrBlock::UpdateAllMoviesPrompt, ]; -pub static COLLECTIONS_BLOCKS: [ActiveRadarrBlock; 6] = [ +pub static COLLECTIONS_BLOCKS: [ActiveRadarrBlock; 7] = [ ActiveRadarrBlock::Collections, + ActiveRadarrBlock::CollectionsSortPrompt, ActiveRadarrBlock::SearchCollection, ActiveRadarrBlock::SearchCollectionError, ActiveRadarrBlock::FilterCollections, diff --git a/src/models/servarr_data/radarr/radarr_data_tests.rs b/src/models/servarr_data/radarr/radarr_data_tests.rs index 0113ca6..1b09d8b 100644 --- a/src/models/servarr_data/radarr/radarr_data_tests.rs +++ b/src/models/servarr_data/radarr/radarr_data_tests.rs @@ -268,8 +268,9 @@ mod tests { #[test] fn test_collections_blocks_contents() { - assert_eq!(COLLECTIONS_BLOCKS.len(), 6); + assert_eq!(COLLECTIONS_BLOCKS.len(), 7); assert!(COLLECTIONS_BLOCKS.contains(&ActiveRadarrBlock::Collections)); + assert!(COLLECTIONS_BLOCKS.contains(&ActiveRadarrBlock::CollectionsSortPrompt)); assert!(COLLECTIONS_BLOCKS.contains(&ActiveRadarrBlock::SearchCollection)); assert!(COLLECTIONS_BLOCKS.contains(&ActiveRadarrBlock::SearchCollectionError)); assert!(COLLECTIONS_BLOCKS.contains(&ActiveRadarrBlock::FilterCollections)); diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index c8af63a..28fb9a5 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -803,8 +803,15 @@ impl<'a, 'b> Network<'a, 'b> { .await; self - .handle_request::<(), Vec>(request_props, |collections_vec, mut app| { - app.data.radarr_data.collections.set_items(collections_vec); + .handle_request::<(), Vec>(request_props, |mut collections_vec, mut app| { + if !matches!( + app.get_current_route(), + Route::Radarr(ActiveRadarrBlock::CollectionsSortPrompt, _) + ) { + collections_vec.sort_by(|a, b| a.id.cmp(&b.id)); + app.data.radarr_data.collections.set_items(collections_vec); + app.data.radarr_data.collections.apply_sorting_toggle(false); + } }) .await; } @@ -1191,9 +1198,14 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::<(), Vec>(request_props, |mut movie_vec, mut app| { - movie_vec.sort_by(|a, b| a.id.cmp(&b.id)); - app.data.radarr_data.movies.set_items(movie_vec); - app.data.radarr_data.movies.apply_sorting_toggle(false); + if !matches!( + app.get_current_route(), + Route::Radarr(ActiveRadarrBlock::MoviesSortPrompt, _) + ) { + movie_vec.sort_by(|a, b| a.id.cmp(&b.id)); + app.data.radarr_data.movies.set_items(movie_vec); + app.data.radarr_data.movies.apply_sorting_toggle(false); + } }) .await; } diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 4a39686..109ee46 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -292,25 +292,26 @@ mod test { ); } + #[rstest] #[tokio::test] - async fn test_handle_get_movies_event() { + async fn test_handle_get_movies_event(#[values(true, false)] use_custom_sorting: bool) { let mut movie_1: Value = serde_json::from_str(MOVIE_JSON).unwrap(); let mut movie_2: Value = serde_json::from_str(MOVIE_JSON).unwrap(); *movie_1.get_mut("id").unwrap() = json!(1); *movie_1.get_mut("title").unwrap() = json!("z test"); *movie_2.get_mut("id").unwrap() = json!(2); *movie_2.get_mut("title").unwrap() = json!("A test"); - let expected_movies = vec![ - Movie { - id: 2, - title: "A test".into(), - ..movie() - }, + let mut expected_movies = vec![ Movie { id: 1, title: "z test".into(), ..movie() }, + Movie { + id: 2, + title: "A test".into(), + ..movie() + }, ]; let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, @@ -321,14 +322,68 @@ mod test { ) .await; app_arc.lock().await.data.radarr_data.movies.sort_asc = true; - let title_sort_option = SortOption { - name: "Title", - cmp_fn: Some(|a: &Movie, b: &Movie| { + if use_custom_sorting { + let cmp_fn = |a: &Movie, b: &Movie| { a.title .text .to_lowercase() .cmp(&b.title.text.to_lowercase()) - }), + }; + expected_movies.sort_by(cmp_fn); + let title_sort_option = SortOption { + name: "Title", + cmp_fn: Some(cmp_fn), + }; + app_arc + .lock() + .await + .data + .radarr_data + .movies + .sorting(vec![title_sort_option]); + } + let mut network = Network::new(&app_arc, CancellationToken::new()); + + network.handle_radarr_event(RadarrEvent::GetMovies).await; + + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.movies.items, + expected_movies + ); + assert!(app_arc.lock().await.data.radarr_data.movies.sort_asc); + } + + #[tokio::test] + async fn test_handle_get_movies_event_no_op_while_user_is_selecting_sort_options() { + let mut movie_1: Value = serde_json::from_str(MOVIE_JSON).unwrap(); + let mut movie_2: Value = serde_json::from_str(MOVIE_JSON).unwrap(); + *movie_1.get_mut("id").unwrap() = json!(1); + *movie_1.get_mut("title").unwrap() = json!("z test"); + *movie_2.get_mut("id").unwrap() = json!(2); + *movie_2.get_mut("title").unwrap() = json!("A test"); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(json!([movie_1, movie_2])), + None, + RadarrEvent::GetMovies.resource(), + ) + .await; + app_arc + .lock() + .await + .push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into()); + app_arc.lock().await.data.radarr_data.movies.sort_asc = true; + let cmp_fn = |a: &Movie, b: &Movie| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }; + let title_sort_option = SortOption { + name: "Title", + cmp_fn: Some(cmp_fn), }; app_arc .lock() @@ -342,10 +397,14 @@ mod test { network.handle_radarr_event(RadarrEvent::GetMovies).await; async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.movies.items, - expected_movies - ); + assert!(app_arc + .lock() + .await + .data + .radarr_data + .movies + .items + .is_empty()); assert!(app_arc.lock().await.data.radarr_data.movies.sort_asc); } @@ -1109,11 +1168,157 @@ mod test { ); } + #[rstest] #[tokio::test] - async fn test_handle_get_collections_event() { - let collection_json = json!([{ + async fn test_handle_get_collections_event(#[values(true, false)] use_custom_sorting: bool) { + let collections_json = json!([{ "id": 123, - "title": "Test Collection", + "title": "z Collection", + "rootFolderPath": "/nfs/movies", + "searchOnAdd": true, + "monitored": true, + "minimumAvailability": "released", + "overview": "Collection blah blah blah", + "qualityProfileId": 2222, + "movies": [{ + "title": "Test", + "overview": "Collection blah blah blah", + "year": 2023, + "runtime": 120, + "tmdbId": 1234, + "genres": ["cool", "family", "fun"], + "ratings": { + "imdb": { + "value": 9.9 + }, + "tmdb": { + "value": 9.9 + }, + "rottenTomatoes": { + "value": 9.9 + } + } + }], + }, + { + "id": 456, + "title": "A Collection", + "rootFolderPath": "/nfs/movies", + "searchOnAdd": true, + "monitored": true, + "minimumAvailability": "released", + "overview": "Collection blah blah blah", + "qualityProfileId": 2222, + "movies": [{ + "title": "Test", + "overview": "Collection blah blah blah", + "year": 2023, + "runtime": 120, + "tmdbId": 1234, + "genres": ["cool", "family", "fun"], + "ratings": { + "imdb": { + "value": 9.9 + }, + "tmdb": { + "value": 9.9 + }, + "rottenTomatoes": { + "value": 9.9 + } + } + }], + }]); + let mut expected_collections = vec![ + Collection { + id: 123, + title: "z Collection".into(), + ..collection() + }, + Collection { + id: 456, + title: "A Collection".into(), + ..collection() + }, + ]; + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(collections_json), + None, + RadarrEvent::GetCollections.resource(), + ) + .await; + app_arc.lock().await.data.radarr_data.collections.sort_asc = true; + if use_custom_sorting { + let cmp_fn = |a: &Collection, b: &Collection| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }; + expected_collections.sort_by(cmp_fn); + + let collection_sort_option = SortOption { + name: "Collection", + cmp_fn: Some(cmp_fn), + }; + app_arc + .lock() + .await + .data + .radarr_data + .collections + .sorting(vec![collection_sort_option]); + } + let mut network = Network::new(&app_arc, CancellationToken::new()); + + network + .handle_radarr_event(RadarrEvent::GetCollections) + .await; + + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.collections.items, + expected_collections + ); + assert!(app_arc.lock().await.data.radarr_data.collections.sort_asc); + } + + #[tokio::test] + async fn test_handle_get_collections_event_no_op_when_user_is_selecting_sort_options() { + let collections_json = json!([{ + "id": 123, + "title": "z Collection", + "rootFolderPath": "/nfs/movies", + "searchOnAdd": true, + "monitored": true, + "minimumAvailability": "released", + "overview": "Collection blah blah blah", + "qualityProfileId": 2222, + "movies": [{ + "title": "Test", + "overview": "Collection blah blah blah", + "year": 2023, + "runtime": 120, + "tmdbId": 1234, + "genres": ["cool", "family", "fun"], + "ratings": { + "imdb": { + "value": 9.9 + }, + "tmdb": { + "value": 9.9 + }, + "rottenTomatoes": { + "value": 9.9 + } + } + }], + }, + { + "id": 456, + "title": "A Collection", "rootFolderPath": "/nfs/movies", "searchOnAdd": true, "monitored": true, @@ -1143,11 +1348,33 @@ mod test { let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Get, None, - Some(collection_json), + Some(collections_json), None, RadarrEvent::GetCollections.resource(), ) .await; + app_arc.lock().await.data.radarr_data.collections.sort_asc = true; + app_arc + .lock() + .await + .push_navigation_stack(ActiveRadarrBlock::CollectionsSortPrompt.into()); + let cmp_fn = |a: &Collection, b: &Collection| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }; + let collection_sort_option = SortOption { + name: "Collection", + cmp_fn: Some(cmp_fn), + }; + app_arc + .lock() + .await + .data + .radarr_data + .collections + .sorting(vec![collection_sort_option]); let mut network = Network::new(&app_arc, CancellationToken::new()); network @@ -1155,10 +1382,15 @@ mod test { .await; async_server.assert_async().await; - assert_eq!( - app_arc.lock().await.data.radarr_data.collections.items, - vec![collection()] - ); + assert!(app_arc + .lock() + .await + .data + .radarr_data + .collections + .items + .is_empty()); + assert!(app_arc.lock().await.data.radarr_data.collections.sort_asc); } #[tokio::test] diff --git a/src/ui/radarr_ui/collections/mod.rs b/src/ui/radarr_ui/collections/mod.rs index b37c14a..81fd139 100644 --- a/src/ui/radarr_ui/collections/mod.rs +++ b/src/ui/radarr_ui/collections/mod.rs @@ -40,7 +40,9 @@ impl DrawUi for CollectionsUi { fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let route = *app.get_current_route(); let mut collections_ui_matcher = |active_radarr_block| match active_radarr_block { - ActiveRadarrBlock::Collections => draw_collections(f, app, area), + ActiveRadarrBlock::Collections | ActiveRadarrBlock::CollectionsSortPrompt => { + draw_collections(f, app, area) + } ActiveRadarrBlock::SearchCollection => draw_popup_over( f, app, @@ -98,69 +100,78 @@ impl DrawUi for CollectionsUi { } pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let current_selection = if !app.data.radarr_data.collections.items.is_empty() { - app.data.radarr_data.collections.current_selection().clone() - } else { - Collection::default() - }; - let quality_profile_map = &app.data.radarr_data.quality_profile_map; - let content = Some(&mut app.data.radarr_data.collections); - let collections_table_footer = app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(); - let collection_row_mapping = |collection: &Collection| { - let number_of_movies = collection.movies.clone().unwrap_or_default().len(); - collection.title.scroll_left_or_reset( - get_width_from_percentage(area, 25), - *collection == current_selection, - app.tick_count % app.ticks_until_scroll == 0, - ); - let monitored = if collection.monitored { "🏷" } else { "" }; - let search_on_add = if collection.search_on_add { - "Yes" + if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + let current_selection = if !app.data.radarr_data.collections.items.is_empty() { + app.data.radarr_data.collections.current_selection().clone() } else { - "No" + Collection::default() }; + let quality_profile_map = &app.data.radarr_data.quality_profile_map; + let content = Some(&mut app.data.radarr_data.collections); + let collections_table_footer = app + .data + .radarr_data + .main_tabs + .get_active_tab_contextual_help(); + let collection_row_mapping = |collection: &Collection| { + let number_of_movies = collection.movies.as_ref().unwrap_or(&Vec::new()).len(); + collection.title.scroll_left_or_reset( + get_width_from_percentage(area, 25), + *collection == current_selection, + app.tick_count % app.ticks_until_scroll == 0, + ); + let monitored = if collection.monitored { "🏷" } else { "" }; + let search_on_add = if collection.search_on_add { + "Yes" + } else { + "No" + }; - Row::new(vec![ - Cell::from(collection.title.to_string()), - Cell::from(number_of_movies.to_string()), - Cell::from(collection.root_folder_path.clone().unwrap_or_default()), - Cell::from( - quality_profile_map - .get_by_left(&collection.quality_profile_id) - .unwrap() - .to_owned(), - ), - Cell::from(search_on_add), - Cell::from(monitored), - ]) - .primary() - }; - let collections_table = ManagarrTable::new(content, collection_row_mapping) - .loading(app.is_loading) - .footer(collections_table_footer) - .block(layout_block_top_border()) - .headers([ - "Collection", - "Number of Movies", - "Root Folder Path", - "Quality Profile", - "Search on Add", - "Monitored", - ]) - .constraints([ - Constraint::Percentage(25), - Constraint::Percentage(15), - Constraint::Percentage(15), - Constraint::Percentage(15), - Constraint::Percentage(15), - Constraint::Percentage(15), - ]); + Row::new(vec![ + Cell::from(collection.title.to_string()), + Cell::from(number_of_movies.to_string()), + Cell::from( + collection + .root_folder_path + .as_ref() + .unwrap_or(&String::new()) + .to_owned(), + ), + Cell::from( + quality_profile_map + .get_by_left(&collection.quality_profile_id) + .unwrap() + .to_owned(), + ), + Cell::from(search_on_add), + Cell::from(monitored), + ]) + .primary() + }; + let collections_table = ManagarrTable::new(content, collection_row_mapping) + .loading(app.is_loading) + .footer(collections_table_footer) + .block(layout_block_top_border()) + .sorting(active_radarr_block == ActiveRadarrBlock::CollectionsSortPrompt) + .headers([ + "Collection", + "Number of Movies", + "Root Folder Path", + "Quality Profile", + "Search on Add", + "Monitored", + ]) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(15), + ]); - f.render_widget(collections_table, area); + f.render_widget(collections_table, area); + } } fn draw_collection_search_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {