From 6cd24be5e4505623c20ef1e5ca9184a422cc8ad3 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 13 Feb 2024 23:00:35 -0700 Subject: [PATCH] Added sorting to the main library table --- src/app/radarr/radarr_context_clues.rs | 3 +- src/app/radarr/radarr_context_clues_tests.rs | 5 + .../library/library_handler_tests.rs | 389 +++++++++++++++++- src/handlers/radarr_handlers/library/mod.rs | 145 ++++++- .../library/movie_details_handler.rs | 9 +- .../library/movie_details_handler_tests.rs | 15 +- src/models/servarr_data/radarr/radarr_data.rs | 4 +- .../servarr_data/radarr/radarr_data_tests.rs | 3 +- src/models/stateful_table.rs | 8 +- src/models/stateful_table_tests.rs | 63 ++- src/network/radarr_network.rs | 3 +- src/network/radarr_network_tests.rs | 2 + src/ui/radarr_ui/library/mod.rs | 175 ++++---- 13 files changed, 707 insertions(+), 117 deletions(-) diff --git a/src/app/radarr/radarr_context_clues.rs b/src/app/radarr/radarr_context_clues.rs index 02d340e..541aa59 100644 --- a/src/app/radarr/radarr_context_clues.rs +++ b/src/app/radarr/radarr_context_clues.rs @@ -5,12 +5,13 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; #[path = "radarr_context_clues_tests.rs"] mod radarr_context_clues_tests; -pub static LIBRARY_CONTEXT_CLUES: [ContextClue; 9] = [ +pub static LIBRARY_CONTEXT_CLUES: [ContextClue; 10] = [ (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.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, diff --git a/src/app/radarr/radarr_context_clues_tests.rs b/src/app/radarr/radarr_context_clues_tests.rs index b78cdb7..07cc68d 100644 --- a/src/app/radarr/radarr_context_clues_tests.rs +++ b/src/app/radarr/radarr_context_clues_tests.rs @@ -42,6 +42,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.refresh); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); diff --git a/src/handlers/radarr_handlers/library/library_handler_tests.rs b/src/handlers/radarr_handlers/library/library_handler_tests.rs index 0fdfb6a..e9c47c0 100644 --- a/src/handlers/radarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/library_handler_tests.rs @@ -1,24 +1,27 @@ #[cfg(test)] mod tests { - use pretty_assertions::assert_str_eq; + use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; + use std::cmp::Ordering; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; - use crate::handlers::radarr_handlers::library::LibraryHandler; + use crate::handlers::radarr_handlers::library::{movies_sorting_options, LibraryHandler}; use crate::handlers::KeyEventHandler; - use crate::models::radarr_models::Movie; + use crate::models::radarr_models::{Language, Movie}; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, ADD_MOVIE_BLOCKS, DELETE_MOVIE_BLOCKS, EDIT_MOVIE_BLOCKS, LIBRARY_BLOCKS, MOVIE_DETAILS_BLOCKS, }; + use crate::models::stateful_table::SortOption; use crate::models::HorizontallyScrollableText; use crate::test_handler_delegation; mod test_handle_scroll_up_and_down { use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; + use pretty_assertions::assert_eq; use super::*; @@ -32,6 +35,51 @@ mod tests { title, to_string ); + + #[rstest] + fn test_movies_sort_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let movie_field_vec = sort_options(); + let mut app = App::default(); + app.data.radarr_data.movies.sorting(sort_options()); + + if key == Key::Up { + for i in (0..movie_field_vec.len()).rev() { + LibraryHandler::with(&key, &mut app, &ActiveRadarrBlock::MoviesSortPrompt, &None) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .sort + .as_ref() + .unwrap() + .current_selection(), + &movie_field_vec[i] + ); + } + } else { + for i in 0..movie_field_vec.len() { + LibraryHandler::with(&key, &mut app, &ActiveRadarrBlock::MoviesSortPrompt, &None) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .sort + .as_ref() + .unwrap() + .current_selection(), + &movie_field_vec[(i + 1) % movie_field_vec.len()] + ); + } + } + } } mod test_handle_home_end { @@ -147,6 +195,53 @@ mod tests { 0 ); } + + #[test] + fn test_movies_sort_home_end() { + let movie_field_vec = sort_options(); + let mut app = App::default(); + app.data.radarr_data.movies.sorting(sort_options()); + + LibraryHandler::with( + &DEFAULT_KEYBINDINGS.end.key, + &mut app, + &ActiveRadarrBlock::MoviesSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .sort + .as_ref() + .unwrap() + .current_selection(), + &movie_field_vec[movie_field_vec.len() - 1] + ); + + LibraryHandler::with( + &DEFAULT_KEYBINDINGS.home.key, + &mut app, + &ActiveRadarrBlock::MoviesSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .movies + .sort + .as_ref() + .unwrap() + .current_selection(), + &movie_field_vec[0] + ); + } } mod test_handle_delete { @@ -579,6 +674,30 @@ mod tests { assert_eq!(app.data.radarr_data.prompt_confirm_action, None); assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); } + + #[test] + fn test_movies_sort_prompt_submit() { + let mut app = App::default(); + app.data.radarr_data.movies.sort_asc = true; + app.data.radarr_data.movies.sorting(sort_options()); + app.data.radarr_data.movies.set_items(movies_vec()); + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into()); + + let mut expected_vec = movies_vec(); + expected_vec.reverse(); + + LibraryHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::MoviesSortPrompt, + &None, + ) + .handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.data.radarr_data.movies.items, expected_vec); + } } mod test_handle_esc { @@ -902,6 +1021,29 @@ mod tests { "h" ); } + + #[test] + fn test_sort_key() { + let mut app = App::default(); + + LibraryHandler::with( + &DEFAULT_KEYBINDINGS.sort.key, + &mut app, + &ActiveRadarrBlock::Movies, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::MoviesSortPrompt.into() + ); + assert_eq!( + app.data.radarr_data.movies.sort.as_ref().unwrap().items, + movies_sorting_options() + ); + assert!(!app.data.radarr_data.movies.sort_asc); + } } #[rstest] @@ -975,6 +1117,247 @@ mod tests { ); } + #[test] + fn test_movies_sorting_options_title() { + let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = |a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }; + let mut expected_movies_vec = movies_vec(); + expected_movies_vec.sort_by(expected_cmp_fn); + + let sort_option = movies_sorting_options()[0].clone(); + let mut sorted_movies_vec = movies_vec(); + sorted_movies_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_movies_vec, expected_movies_vec); + assert_str_eq!(sort_option.name, "Title"); + } + + #[test] + fn test_movies_sorting_options_year() { + let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = |a, b| a.year.cmp(&b.year); + let mut expected_movies_vec = movies_vec(); + expected_movies_vec.sort_by(expected_cmp_fn); + + let sort_option = movies_sorting_options()[1].clone(); + let mut sorted_movies_vec = movies_vec(); + sorted_movies_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_movies_vec, expected_movies_vec); + assert_str_eq!(sort_option.name, "Year"); + } + + #[test] + fn test_movies_sorting_options_studio() { + let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = + |a, b| a.studio.to_lowercase().cmp(&b.studio.to_lowercase()); + let mut expected_movies_vec = movies_vec(); + expected_movies_vec.sort_by(expected_cmp_fn); + + let sort_option = movies_sorting_options()[2].clone(); + let mut sorted_movies_vec = movies_vec(); + sorted_movies_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_movies_vec, expected_movies_vec); + assert_str_eq!(sort_option.name, "Studio"); + } + + #[test] + fn test_movies_sorting_options_runtime() { + let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = |a, b| a.runtime.cmp(&b.runtime); + let mut expected_movies_vec = movies_vec(); + expected_movies_vec.sort_by(expected_cmp_fn); + + let sort_option = movies_sorting_options()[3].clone(); + let mut sorted_movies_vec = movies_vec(); + sorted_movies_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_movies_vec, expected_movies_vec); + assert_str_eq!(sort_option.name, "Runtime"); + } + + #[test] + fn test_movies_sorting_options_rating() { + let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = |a, b| { + a.certification + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase() + .cmp( + &b.certification + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase(), + ) + }; + let mut expected_movies_vec = movies_vec(); + expected_movies_vec.sort_by(expected_cmp_fn); + + let sort_option = movies_sorting_options()[4].clone(); + let mut sorted_movies_vec = movies_vec(); + sorted_movies_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_movies_vec, expected_movies_vec); + assert_str_eq!(sort_option.name, "Rating"); + } + + #[test] + fn test_movies_sorting_options_language() { + let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = |a, b| { + a.original_language + .name + .to_lowercase() + .cmp(&b.original_language.name.to_lowercase()) + }; + let mut expected_movies_vec = movies_vec(); + expected_movies_vec.sort_by(expected_cmp_fn); + + let sort_option = movies_sorting_options()[5].clone(); + let mut sorted_movies_vec = movies_vec(); + sorted_movies_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_movies_vec, expected_movies_vec); + assert_str_eq!(sort_option.name, "Language"); + } + + #[test] + fn test_movies_sorting_options_size() { + let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = + |a, b| a.size_on_disk.cmp(&b.size_on_disk); + let mut expected_movies_vec = movies_vec(); + expected_movies_vec.sort_by(expected_cmp_fn); + + let sort_option = movies_sorting_options()[6].clone(); + let mut sorted_movies_vec = movies_vec(); + sorted_movies_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_movies_vec, expected_movies_vec); + assert_str_eq!(sort_option.name, "Size"); + } + + #[test] + fn test_movies_sorting_options_quality() { + let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = + |a, b| a.quality_profile_id.cmp(&b.quality_profile_id); + let mut expected_movies_vec = movies_vec(); + expected_movies_vec.sort_by(expected_cmp_fn); + + let sort_option = movies_sorting_options()[7].clone(); + let mut sorted_movies_vec = movies_vec(); + sorted_movies_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_movies_vec, expected_movies_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + #[test] + fn test_movies_sorting_options_monitored() { + let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = |a, b| a.monitored.cmp(&b.monitored); + let mut expected_movies_vec = movies_vec(); + expected_movies_vec.sort_by(expected_cmp_fn); + + let sort_option = movies_sorting_options()[8].clone(); + let mut sorted_movies_vec = movies_vec(); + sorted_movies_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_movies_vec, expected_movies_vec); + assert_str_eq!(sort_option.name, "Monitored"); + } + + #[test] + fn test_movies_sorting_options_tags() { + let expected_cmp_fn: fn(&Movie, &Movie) -> Ordering = |a, b| { + let a_str = a + .tags + .iter() + .map(|tag| tag.as_i64().unwrap().to_string()) + .collect::>() + .join(","); + let b_str = b + .tags + .iter() + .map(|tag| tag.as_i64().unwrap().to_string()) + .collect::>() + .join(","); + + a_str.cmp(&b_str) + }; + let mut expected_movies_vec = movies_vec(); + expected_movies_vec.sort_by(expected_cmp_fn); + + let sort_option = movies_sorting_options()[9].clone(); + let mut sorted_movies_vec = movies_vec(); + sorted_movies_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_movies_vec, expected_movies_vec); + assert_str_eq!(sort_option.name, "Tags"); + } + + fn movies_vec() -> Vec { + vec![ + Movie { + title: "test 1".into(), + original_language: Language { + name: "English".to_owned(), + }, + size_on_disk: 1024, + studio: "Studio 1".to_owned(), + year: 2024, + monitored: false, + runtime: 12.into(), + quality_profile_id: 1, + certification: Some("PG-13".to_owned()), + tags: vec![1.into(), 2.into()], + ..Movie::default() + }, + Movie { + title: "test 2".into(), + original_language: Language { + name: "Chinese".to_owned(), + }, + size_on_disk: 2048, + studio: "Studio 2".to_owned(), + year: 1998, + monitored: false, + runtime: 60.into(), + quality_profile_id: 2, + certification: Some("R".to_owned()), + tags: vec![1.into(), 3.into()], + ..Movie::default() + }, + Movie { + title: "test 3".into(), + original_language: Language { + name: "Japanese".to_owned(), + }, + size_on_disk: 512, + studio: "studio 3".to_owned(), + year: 1954, + monitored: true, + runtime: 120.into(), + quality_profile_id: 3, + certification: Some("G".to_owned()), + tags: vec![2.into(), 3.into()], + ..Movie::default() + }, + ] + } + + fn sort_options() -> Vec> { + vec![SortOption { + name: "Test 1", + cmp_fn: Some(|a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }), + }] + } + #[test] fn test_library_handler_accepts() { let mut library_handler_blocks = Vec::new(); diff --git a/src/handlers/radarr_handlers/library/mod.rs b/src/handlers/radarr_handlers/library/mod.rs index 1689287..c173552 100644 --- a/src/handlers/radarr_handlers/library/mod.rs +++ b/src/handlers/radarr_handlers/library/mod.rs @@ -8,9 +8,11 @@ use crate::handlers::radarr_handlers::library::edit_movie_handler::EditMovieHand use crate::handlers::radarr_handlers::library::movie_details_handler::MovieDetailsHandler; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; +use crate::models::radarr_models::Movie; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, DELETE_MOVIE_SELECTION_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS, LIBRARY_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}; @@ -79,14 +81,34 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' } fn handle_scroll_up(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Movies { - self.app.data.radarr_data.movies.scroll_up() + match self.active_radarr_block { + ActiveRadarrBlock::Movies => self.app.data.radarr_data.movies.scroll_up(), + ActiveRadarrBlock::MoviesSortPrompt => self + .app + .data + .radarr_data + .movies + .sort + .as_mut() + .unwrap() + .scroll_up(), + _ => (), } } fn handle_scroll_down(&mut self) { - if self.active_radarr_block == &ActiveRadarrBlock::Movies { - self.app.data.radarr_data.movies.scroll_down() + match self.active_radarr_block { + ActiveRadarrBlock::Movies => self.app.data.radarr_data.movies.scroll_down(), + ActiveRadarrBlock::MoviesSortPrompt => self + .app + .data + .radarr_data + .movies + .sort + .as_mut() + .unwrap() + .scroll_down(), + _ => (), } } @@ -115,6 +137,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' .unwrap() .scroll_home(); } + ActiveRadarrBlock::MoviesSortPrompt => self + .app + .data + .radarr_data + .movies + .sort + .as_mut() + .unwrap() + .scroll_to_top(), _ => (), } } @@ -140,6 +171,15 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' .as_mut() .unwrap() .reset_offset(), + ActiveRadarrBlock::MoviesSortPrompt => self + .app + .data + .radarr_data + .movies + .sort + .as_mut() + .unwrap() + .scroll_to_bottom(), _ => (), } } @@ -226,6 +266,11 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' self.app.pop_navigation_stack(); } + ActiveRadarrBlock::MoviesSortPrompt => { + self.app.data.radarr_data.movies.apply_sorting(); + + self.app.pop_navigation_stack(); + } _ => (), } } @@ -300,6 +345,17 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { self.app.should_refresh = true; } + _ if *key == DEFAULT_KEYBINDINGS.sort.key => { + self + .app + .data + .radarr_data + .movies + .sorting(movies_sorting_options()); + self + .app + .push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into()); + } _ => (), }, ActiveRadarrBlock::SearchMovie => { @@ -320,3 +376,84 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' } } } + +fn movies_sorting_options() -> Vec> { + vec![ + SortOption { + name: "Title", + cmp_fn: Some(|a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }), + }, + SortOption { + name: "Year", + cmp_fn: Some(|a, b| a.year.cmp(&b.year)), + }, + SortOption { + name: "Studio", + cmp_fn: Some(|a, b| a.studio.to_lowercase().cmp(&b.studio.to_lowercase())), + }, + SortOption { + name: "Runtime", + cmp_fn: Some(|a, b| a.runtime.cmp(&b.runtime)), + }, + SortOption { + name: "Rating", + cmp_fn: Some(|a, b| { + a.certification + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase() + .cmp( + &b.certification + .as_ref() + .unwrap_or(&String::new()) + .to_lowercase(), + ) + }), + }, + SortOption { + name: "Language", + cmp_fn: Some(|a, b| { + a.original_language + .name + .to_lowercase() + .cmp(&b.original_language.name.to_lowercase()) + }), + }, + SortOption { + name: "Size", + cmp_fn: Some(|a, b| a.size_on_disk.cmp(&b.size_on_disk)), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| a.quality_profile_id.cmp(&b.quality_profile_id)), + }, + SortOption { + name: "Monitored", + cmp_fn: Some(|a, b| a.monitored.cmp(&b.monitored)), + }, + SortOption { + name: "Tags", + cmp_fn: Some(|a, b| { + let a_str = a + .tags + .iter() + .map(|tag| tag.as_i64().unwrap().to_string()) + .collect::>() + .join(","); + let b_str = b + .tags + .iter() + .map(|tag| tag.as_i64().unwrap().to_string()) + .collect::>() + .join(","); + + a_str.cmp(&b_str) + }), + }, + ] +} diff --git a/src/handlers/radarr_handlers/library/movie_details_handler.rs b/src/handlers/radarr_handlers/library/movie_details_handler.rs index 955d32d..1e04aff 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler.rs @@ -469,11 +469,16 @@ fn releases_sorting_options() -> Vec> { }, SortOption { name: "Title", - cmp_fn: Some(|a, b| a.title.text.cmp(&b.title.text)), + cmp_fn: Some(|a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }), }, SortOption { name: "Indexer", - cmp_fn: Some(|a, b| a.indexer.cmp(&b.indexer)), + cmp_fn: Some(|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase())), }, SortOption { name: "Size", diff --git a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs index 390268b..f4dec9b 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs @@ -1108,8 +1108,12 @@ mod tests { #[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 expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| { + a.title + .text + .to_lowercase() + .cmp(&b.title.text.to_lowercase()) + }; let mut expected_releases_vec = release_vec(); expected_releases_vec.sort_by(expected_cmp_fn); @@ -1123,7 +1127,8 @@ mod tests { #[test] fn test_releases_sorting_options_indexer() { - let expected_cmp_fn: fn(&Release, &Release) -> Ordering = |a, b| a.indexer.cmp(&b.indexer); + let expected_cmp_fn: fn(&Release, &Release) -> Ordering = + |a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase()); let mut expected_releases_vec = release_vec(); expected_releases_vec.sort_by(expected_cmp_fn); @@ -1237,8 +1242,8 @@ mod tests { let release_b = Release { protocol: "Protocol B".to_owned(), age: 2, - title: HorizontallyScrollableText::from("Title B"), - indexer: "Indexer B".to_owned(), + title: HorizontallyScrollableText::from("title B"), + indexer: "indexer B".to_owned(), size: 2, rejected: false, seeders: Some(Number::from(2)), diff --git a/src/models/servarr_data/radarr/radarr_data.rs b/src/models/servarr_data/radarr/radarr_data.rs index 1ca7221..e98d696 100644 --- a/src/models/servarr_data/radarr/radarr_data.rs +++ b/src/models/servarr_data/radarr/radarr_data.rs @@ -270,6 +270,7 @@ pub enum ActiveRadarrBlock { MovieHistory, #[default] Movies, + MoviesSortPrompt, RootFolders, System, SystemLogs, @@ -289,8 +290,9 @@ pub enum ActiveRadarrBlock { ViewMovieOverview, } -pub static LIBRARY_BLOCKS: [ActiveRadarrBlock; 6] = [ +pub static LIBRARY_BLOCKS: [ActiveRadarrBlock; 7] = [ ActiveRadarrBlock::Movies, + ActiveRadarrBlock::MoviesSortPrompt, ActiveRadarrBlock::SearchMovie, ActiveRadarrBlock::SearchMovieError, ActiveRadarrBlock::FilterMovies, diff --git a/src/models/servarr_data/radarr/radarr_data_tests.rs b/src/models/servarr_data/radarr/radarr_data_tests.rs index 6f33cde..0113ca6 100644 --- a/src/models/servarr_data/radarr/radarr_data_tests.rs +++ b/src/models/servarr_data/radarr/radarr_data_tests.rs @@ -256,8 +256,9 @@ mod tests { #[test] fn test_library_blocks_contents() { - assert_eq!(LIBRARY_BLOCKS.len(), 6); + assert_eq!(LIBRARY_BLOCKS.len(), 7); assert!(LIBRARY_BLOCKS.contains(&ActiveRadarrBlock::Movies)); + assert!(LIBRARY_BLOCKS.contains(&ActiveRadarrBlock::MoviesSortPrompt)); assert!(LIBRARY_BLOCKS.contains(&ActiveRadarrBlock::SearchMovie)); assert!(LIBRARY_BLOCKS.contains(&ActiveRadarrBlock::SearchMovieError)); assert!(LIBRARY_BLOCKS.contains(&ActiveRadarrBlock::FilterMovies)); diff --git a/src/models/stateful_table.rs b/src/models/stateful_table.rs index e4f2ade..a130cad 100644 --- a/src/models/stateful_table.rs +++ b/src/models/stateful_table.rs @@ -218,8 +218,14 @@ where } pub fn apply_sorting(&mut self) { + self.apply_sorting_toggle(true); + } + + pub fn apply_sorting_toggle(&mut self, toggle_dir: bool) { if let Some(sort_options) = &mut self.sort { - self.sort_asc = !self.sort_asc; + if toggle_dir { + 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 { diff --git a/src/models/stateful_table_tests.rs b/src/models/stateful_table_tests.rs index 9465632..42a3483 100644 --- a/src/models/stateful_table_tests.rs +++ b/src/models/stateful_table_tests.rs @@ -268,18 +268,18 @@ mod tests { } #[test] - fn test_stateful_table_apply_sorting_no_op_no_sort_options() { + fn test_stateful_table_apply_sorting_toggle_no_op_no_sort_options() { let mut stateful_table = create_test_stateful_table(); let expected_items = stateful_table.items.clone(); - stateful_table.apply_sorting(); + stateful_table.apply_sorting_toggle(true); assert_eq!(stateful_table.items, expected_items); assert!(!stateful_table.sort_asc); } #[test] - fn test_stateful_table_apply_sorting_no_op_no_cmp_fn() { + fn test_stateful_table_apply_sorting_toggle_no_op_no_cmp_fn() { let mut stateful_table = create_test_stateful_table(); stateful_table.sorting(vec![SortOption { name: "Test 1", @@ -287,14 +287,14 @@ mod tests { }]); let expected_items = stateful_table.items.clone(); - stateful_table.apply_sorting(); + stateful_table.apply_sorting_toggle(true); 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() { + fn test_filtered_stateful_table_apply_sorting_toggle_no_op_no_cmp_fn() { let mut filtered_stateful_table = create_test_filtered_stateful_table(); filtered_stateful_table.sorting(vec![SortOption { name: "Test 1", @@ -306,7 +306,7 @@ mod tests { .unwrap() .clone(); - filtered_stateful_table.apply_sorting(); + filtered_stateful_table.apply_sorting_toggle(true); assert_eq!( *filtered_stateful_table.filtered_items.as_ref().unwrap(), @@ -316,7 +316,7 @@ mod tests { } #[test] - fn test_stateful_table_apply_sorting() { + fn test_stateful_table_apply_sorting_toggles_direction() { let mut stateful_table = create_test_stateful_table(); stateful_table.sorting(vec![SortOption { name: "Test 1", @@ -325,12 +325,12 @@ mod tests { let mut expected_items = stateful_table.items.clone(); expected_items.sort(); - stateful_table.apply_sorting(); + stateful_table.apply_sorting_toggle(true); assert_eq!(stateful_table.items, expected_items); assert!(stateful_table.sort_asc); - stateful_table.apply_sorting(); + stateful_table.apply_sorting_toggle(true); expected_items.reverse(); assert_eq!(stateful_table.items, expected_items); @@ -338,7 +338,46 @@ mod tests { } #[test] - fn test_filtered_stateful_table_apply_sorting() { + fn test_stateful_table_apply_sorting_toggle() { + 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_toggle(true); + + assert_eq!(stateful_table.items, expected_items); + assert!(stateful_table.sort_asc); + + stateful_table.apply_sorting_toggle(true); + + expected_items.reverse(); + assert_eq!(stateful_table.items, expected_items); + assert!(!stateful_table.sort_asc); + } + + #[test] + fn test_stateful_table_apply_sorting_toggle_false_doesnt_toggle_direction() { + 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(); + expected_items.reverse(); + + stateful_table.apply_sorting_toggle(false); + + assert_eq!(stateful_table.items, expected_items); + assert!(!stateful_table.sort_asc); + } + + #[test] + fn test_filtered_stateful_table_apply_sorting_toggle() { let mut filtered_stateful_table = create_test_filtered_stateful_table(); filtered_stateful_table.sorting(vec![SortOption { name: "Test 1", @@ -351,7 +390,7 @@ mod tests { .clone(); expected_items.sort(); - filtered_stateful_table.apply_sorting(); + filtered_stateful_table.apply_sorting_toggle(true); assert_eq!( *filtered_stateful_table.filtered_items.as_ref().unwrap(), @@ -359,7 +398,7 @@ mod tests { ); assert!(filtered_stateful_table.sort_asc); - filtered_stateful_table.apply_sorting(); + filtered_stateful_table.apply_sorting_toggle(true); expected_items.reverse(); assert_eq!( diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 6a9feef..6109dec 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -1191,7 +1191,8 @@ impl<'a, 'b> Network<'a, 'b> { self .handle_request::<(), Vec>(request_props, |movie_vec, mut app| { - app.data.radarr_data.movies.set_items(movie_vec) + 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 6b83b7c..98119a0 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -301,6 +301,7 @@ mod test { RadarrEvent::GetMovies.resource(), ) .await; + app_arc.lock().await.data.radarr_data.movies.sort_asc = true; let mut network = Network::new(&app_arc, CancellationToken::new()); network.handle_radarr_event(RadarrEvent::GetMovies).await; @@ -310,6 +311,7 @@ mod test { app_arc.lock().await.data.radarr_data.movies.items, vec![movie()] ); + assert!(app_arc.lock().await.data.radarr_data.movies.sort_asc); } #[tokio::test] diff --git a/src/ui/radarr_ui/library/mod.rs b/src/ui/radarr_ui/library/mod.rs index 1c5b79f..6e6b2c6 100644 --- a/src/ui/radarr_ui/library/mod.rs +++ b/src/ui/radarr_ui/library/mod.rs @@ -46,7 +46,7 @@ impl DrawUi for LibraryUi { let route = *app.get_current_route(); let mut library_ui_matchers = |active_radarr_block: ActiveRadarrBlock| match active_radarr_block { - ActiveRadarrBlock::Movies => draw_library(f, app, area), + ActiveRadarrBlock::Movies | ActiveRadarrBlock::MoviesSortPrompt => draw_library(f, app, area), ActiveRadarrBlock::SearchMovie => draw_popup_over( f, app, @@ -101,94 +101,97 @@ impl DrawUi for LibraryUi { } pub(super) fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let current_selection = if !app.data.radarr_data.movies.items.is_empty() { - app.data.radarr_data.movies.current_selection().clone() - } else { - Movie::default() - }; - let quality_profile_map = &app.data.radarr_data.quality_profile_map; - let tags_map = &app.data.radarr_data.tags_map; - let downloads_vec = &app.data.radarr_data.downloads.items; - let content = Some(&mut app.data.radarr_data.movies); - let help_footer = app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(); + if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + let current_selection = if !app.data.radarr_data.movies.items.is_empty() { + app.data.radarr_data.movies.current_selection().clone() + } else { + Movie::default() + }; + let quality_profile_map = &app.data.radarr_data.quality_profile_map; + let tags_map = &app.data.radarr_data.tags_map; + let downloads_vec = &app.data.radarr_data.downloads.items; + let content = Some(&mut app.data.radarr_data.movies); + let help_footer = app + .data + .radarr_data + .main_tabs + .get_active_tab_contextual_help(); - let library_table_row_mapping = |movie: &Movie| { - movie.title.scroll_left_or_reset( - get_width_from_percentage(area, 27), - *movie == current_selection, - app.tick_count % app.ticks_until_scroll == 0, - ); - let monitored = if movie.monitored { "🏷" } else { "" }; - let (hours, minutes) = convert_runtime(movie.runtime); - let file_size: f64 = convert_to_gb(movie.size_on_disk); - let certification = movie.certification.clone().unwrap_or_default(); - let quality_profile = quality_profile_map - .get_by_left(&movie.quality_profile_id) - .unwrap() - .to_owned(); - let tags = movie - .tags - .iter() - .map(|tag_id| { - tags_map - .get_by_left(&tag_id.as_i64().unwrap()) - .unwrap() - .clone() - }) - .collect::>() - .join(", "); + let library_table_row_mapping = |movie: &Movie| { + movie.title.scroll_left_or_reset( + get_width_from_percentage(area, 27), + *movie == current_selection, + app.tick_count % app.ticks_until_scroll == 0, + ); + let monitored = if movie.monitored { "🏷" } else { "" }; + let (hours, minutes) = convert_runtime(movie.runtime); + let file_size: f64 = convert_to_gb(movie.size_on_disk); + let certification = movie.certification.clone().unwrap_or_default(); + let quality_profile = quality_profile_map + .get_by_left(&movie.quality_profile_id) + .unwrap() + .to_owned(); + let tags = movie + .tags + .iter() + .map(|tag_id| { + tags_map + .get_by_left(&tag_id.as_i64().unwrap()) + .unwrap() + .clone() + }) + .collect::>() + .join(", "); - decorate_with_row_style( - downloads_vec, - movie, - Row::new(vec![ - Cell::from(movie.title.to_string()), - Cell::from(movie.year.to_string()), - Cell::from(movie.studio.to_string()), - Cell::from(format!("{hours}h {minutes}m")), - Cell::from(certification), - Cell::from(movie.original_language.name.to_owned()), - Cell::from(format!("{file_size:.2} GB")), - Cell::from(quality_profile), - Cell::from(monitored.to_owned()), - Cell::from(tags), - ]), - ) - }; - let library_table = ManagarrTable::new(content, library_table_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading) - .footer(help_footer) - .headers([ - "Title", - "Year", - "Studio", - "Runtime", - "Rating", - "Language", - "Size", - "Quality Profile", - "Monitored", - "Tags", - ]) - .constraints([ - Constraint::Percentage(27), - Constraint::Percentage(4), - Constraint::Percentage(17), - Constraint::Percentage(6), - Constraint::Percentage(6), - Constraint::Percentage(6), - Constraint::Percentage(6), - Constraint::Percentage(10), - Constraint::Percentage(6), - Constraint::Percentage(12), - ]); + decorate_with_row_style( + downloads_vec, + movie, + Row::new(vec![ + Cell::from(movie.title.to_string()), + Cell::from(movie.year.to_string()), + Cell::from(movie.studio.to_string()), + Cell::from(format!("{hours}h {minutes}m")), + Cell::from(certification), + Cell::from(movie.original_language.name.to_owned()), + Cell::from(format!("{file_size:.2} GB")), + Cell::from(quality_profile), + Cell::from(monitored.to_owned()), + Cell::from(tags), + ]), + ) + }; + let library_table = ManagarrTable::new(content, library_table_row_mapping) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(help_footer) + .sorting(active_radarr_block == ActiveRadarrBlock::MoviesSortPrompt) + .headers([ + "Title", + "Year", + "Studio", + "Runtime", + "Rating", + "Language", + "Size", + "Quality Profile", + "Monitored", + "Tags", + ]) + .constraints([ + Constraint::Percentage(27), + Constraint::Percentage(4), + Constraint::Percentage(17), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(10), + Constraint::Percentage(6), + Constraint::Percentage(12), + ]); - f.render_widget(library_table, area); + f.render_widget(library_table, area); + } } fn draw_update_all_movies_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {