From bf5ecbd5a6d0c177a5c550c64f4edaf54c08f37d Mon Sep 17 00:00:00 2001 From: Dark-Alex-17 Date: Tue, 8 Aug 2023 10:50:06 -0600 Subject: [PATCH] Implemented the ability to edit collections and fixed a refresh bug so screens will automatically refresh when users edit movies or collections --- Cargo.toml | 2 +- README.md | 4 +- src/app/mod.rs | 4 +- src/app/radarr.rs | 611 ++++++++++++--- .../radarr_handlers/add_movie_handler.rs | 88 +-- .../collection_details_handler.rs | 57 +- .../edit_collection_handler.rs | 693 ++++++++++++++++++ .../radarr_handlers/edit_movie_handler.rs | 62 +- src/handlers/radarr_handlers/mod.rs | 136 +++- src/models/radarr_models.rs | 4 + src/network/radarr_network.rs | 290 +++++++- src/ui/radarr_ui/add_movie_ui.rs | 8 +- src/ui/radarr_ui/collection_details_ui.rs | 29 +- src/ui/radarr_ui/edit_collection_ui.rs | 194 +++++ src/ui/radarr_ui/edit_movie_ui.rs | 65 +- src/ui/radarr_ui/mod.rs | 57 +- src/ui/radarr_ui/movie_details_ui.rs | 7 +- src/utils.rs | 2 +- 18 files changed, 2040 insertions(+), 273 deletions(-) create mode 100644 src/handlers/radarr_handlers/edit_collection_handler.rs create mode 100644 src/ui/radarr_ui/edit_collection_ui.rs diff --git a/Cargo.toml b/Cargo.toml index 8cea92e..a88de84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managarr" -version = "0.0.13" +version = "0.0.14" authors = ["Alex Clarke "] description = "A TUI for managing *arr servers" keywords = ["managarr", "tui-rs", "dashboard", "servarr"] diff --git a/README.md b/README.md index 2048cf0..27ca4fc 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,10 @@ tautulli: - [x] Trigger automatic searches for movies - [x] Trigger refresh and disk scan for movies, downloads, and collections - [x] Manually search for movies -- [x] Edit movies +- [x] Edit your movies and collections +- [x] Manage your tags - [ ] Manage your quality profiles - [ ] Manage your quality definitions -- [x] Manage your tags - [ ] Manage your indexers ### Sonarr diff --git a/src/app/mod.rs b/src/app/mod.rs index 89cb893..1d376cc 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -194,7 +194,7 @@ mod tests { assert_eq!(app.navigation_stack, vec![DEFAULT_ROUTE]); assert!(app.network_tx.is_none()); assert_eq!(app.error, HorizontallyScrollableText::default()); - assert_eq!(app.response, String::default()); + assert!(app.response.is_empty()); assert_eq!(app.server_tabs.index, 0); assert_eq!( app.server_tabs.tabs, @@ -289,7 +289,7 @@ mod tests { assert_eq!(app.tick_count, 0); assert_eq!(app.error, HorizontallyScrollableText::default()); - assert_eq!(app.data.radarr_data.version, String::default()); + assert!(app.data.radarr_data.version.is_empty()); } #[test] diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 4ea5ea8..54649ea 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -20,9 +20,9 @@ pub struct RadarrData { pub movies: StatefulTable, pub filtered_movies: StatefulTable, pub add_searched_movies: StatefulTable, - pub movie_monitor_list: StatefulList, - pub movie_minimum_availability_list: StatefulList, - pub movie_quality_profile_list: StatefulList, + pub monitor_list: StatefulList, + pub minimum_availability_list: StatefulList, + pub quality_profile_list: StatefulList, pub selected_block: ActiveRadarrBlock, pub downloads: StatefulTable, pub quality_profile_map: BiMap, @@ -47,6 +47,7 @@ pub struct RadarrData { pub edit_path: HorizontallyScrollableText, pub edit_tags: HorizontallyScrollableText, pub edit_monitored: Option, + pub edit_search_on_add: Option, pub sort_ascending: Option, pub prompt_confirm: bool, pub is_searching: bool, @@ -74,11 +75,12 @@ impl RadarrData { self.filtered_collections = StatefulTable::default(); } - pub fn reset_add_edit_movie_fields(&mut self) { + pub fn reset_add_edit_media_fields(&mut self) { self.edit_monitored = None; + self.edit_search_on_add = None; self.edit_path = HorizontallyScrollableText::default(); self.edit_tags = HorizontallyScrollableText::default(); - self.reset_movie_preferences_selections(); + self.reset_preferences_selections(); } pub fn reset_movie_info_tabs(&mut self) { @@ -95,29 +97,25 @@ impl RadarrData { self.movie_info_tabs.index = 0; } - pub fn reset_movie_preferences_selections(&mut self) { - self.movie_monitor_list = StatefulList::default(); - self.movie_minimum_availability_list = StatefulList::default(); - self.movie_quality_profile_list = StatefulList::default(); + pub fn reset_preferences_selections(&mut self) { + self.monitor_list = StatefulList::default(); + self.minimum_availability_list = StatefulList::default(); + self.quality_profile_list = StatefulList::default(); } - pub fn populate_movie_preferences_lists(&mut self) { + pub fn populate_preferences_lists(&mut self) { + self.monitor_list.set_items(Vec::from_iter(Monitor::iter())); self - .movie_monitor_list - .set_items(Vec::from_iter(Monitor::iter())); - self - .movie_minimum_availability_list + .minimum_availability_list .set_items(Vec::from_iter(MinimumAvailability::iter())); let mut quality_profile_names: Vec = self.quality_profile_map.right_values().cloned().collect(); quality_profile_names.sort(); - self - .movie_quality_profile_list - .set_items(quality_profile_names); + self.quality_profile_list.set_items(quality_profile_names); } pub fn populate_edit_movie_fields(&mut self) { - self.populate_movie_preferences_lists(); + self.populate_preferences_lists(); let Movie { path, tags, @@ -147,12 +145,12 @@ impl RadarrData { self.edit_monitored = Some(*monitored); let minimum_availability_index = self - .movie_minimum_availability_list + .minimum_availability_list .items .iter() .position(|ma| ma == minimum_availability); self - .movie_minimum_availability_list + .minimum_availability_list .state .select(minimum_availability_index); @@ -161,12 +159,56 @@ impl RadarrData { .get_by_left(&quality_profile_id.as_u64().unwrap()) .unwrap(); let quality_profile_index = self - .movie_quality_profile_list + .quality_profile_list .items .iter() .position(|profile| profile == quality_profile_name); self - .movie_quality_profile_list + .quality_profile_list + .state + .select(quality_profile_index); + } + + pub fn populate_edit_collection_fields(&mut self) { + self.populate_preferences_lists(); + let Collection { + root_folder_path, + monitored, + search_on_add, + minimum_availability, + quality_profile_id, + .. + } = if self.filtered_collections.items.is_empty() { + self.collections.current_selection() + } else { + self.filtered_collections.current_selection() + }; + + self.edit_path = root_folder_path.clone().unwrap_or_default().into(); + self.edit_monitored = Some(*monitored); + self.edit_search_on_add = Some(*search_on_add); + + let minimum_availability_index = self + .minimum_availability_list + .items + .iter() + .position(|ma| ma == minimum_availability); + self + .minimum_availability_list + .state + .select(minimum_availability_index); + + let quality_profile_name = self + .quality_profile_map + .get_by_left(&quality_profile_id.as_u64().unwrap()) + .unwrap(); + let quality_profile_index = self + .quality_profile_list + .items + .iter() + .position(|profile| profile == quality_profile_name); + self + .quality_profile_list .state .select(quality_profile_index); } @@ -181,9 +223,9 @@ impl Default for RadarrData { start_time: DateTime::default(), movies: StatefulTable::default(), add_searched_movies: StatefulTable::default(), - movie_monitor_list: StatefulList::default(), - movie_minimum_availability_list: StatefulList::default(), - movie_quality_profile_list: StatefulList::default(), + monitor_list: StatefulList::default(), + minimum_availability_list: StatefulList::default(), + quality_profile_list: StatefulList::default(), selected_block: ActiveRadarrBlock::AddMovieSelectMonitor, filtered_movies: StatefulTable::default(), downloads: StatefulTable::default(), @@ -207,6 +249,7 @@ impl Default for RadarrData { edit_path: HorizontallyScrollableText::default(), edit_tags: HorizontallyScrollableText::default(), edit_monitored: None, + edit_search_on_add: None, sort_ascending: None, is_searching: false, is_filtering: false, @@ -229,7 +272,7 @@ impl Default for RadarrData { title: "Collections".to_owned(), route: ActiveRadarrBlock::Collections.into(), help: String::default(), - contextual_help: Some(" search | filter | refresh | update all | details | cancel filter" + contextual_help: Some(" search | edit | filter | refresh | update all | details | cancel filter" .to_owned()), }, ]), @@ -295,6 +338,13 @@ pub enum ActiveRadarrBlock { DeleteMoviePrompt, DeleteDownloadPrompt, Downloads, + EditCollectionPrompt, + EditCollectionConfirmPrompt, + EditCollectionRootFolderPathInput, + EditCollectionSelectMinimumAvailability, + EditCollectionSelectQualityProfile, + EditCollectionToggleSearchOnAdd, + EditCollectionToggleMonitored, EditMoviePrompt, EditMovieConfirmPrompt, EditMoviePathInput, @@ -331,6 +381,15 @@ pub const ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 9] = [ ActiveRadarrBlock::AddMovieAlreadyInLibrary, ActiveRadarrBlock::AddMovieTagsInput, ]; +pub const EDIT_COLLECTION_BLOCKS: [ActiveRadarrBlock; 7] = [ + ActiveRadarrBlock::EditCollectionPrompt, + ActiveRadarrBlock::EditCollectionConfirmPrompt, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + ActiveRadarrBlock::EditCollectionSelectQualityProfile, + ActiveRadarrBlock::EditCollectionToggleSearchOnAdd, + ActiveRadarrBlock::EditCollectionToggleMonitored, +]; pub const EDIT_MOVIE_BLOCKS: [ActiveRadarrBlock; 7] = [ ActiveRadarrBlock::EditMoviePrompt, ActiveRadarrBlock::EditMovieConfirmPrompt, @@ -366,7 +425,7 @@ pub const FILTER_BLOCKS: [ActiveRadarrBlock; 2] = [ ]; impl ActiveRadarrBlock { - pub fn next_add_prompt_block(&self) -> Self { + pub fn next_add_movie_prompt_block(&self) -> Self { match self { ActiveRadarrBlock::AddMovieSelectMonitor => { ActiveRadarrBlock::AddMovieSelectMinimumAvailability @@ -380,7 +439,7 @@ impl ActiveRadarrBlock { } } - pub fn next_edit_prompt_block(&self) -> Self { + pub fn next_edit_movie_prompt_block(&self) -> Self { match self { ActiveRadarrBlock::EditMovieToggleMonitored => { ActiveRadarrBlock::EditMovieSelectMinimumAvailability @@ -395,7 +454,28 @@ impl ActiveRadarrBlock { } } - pub fn previous_add_prompt_block(&self) -> Self { + pub fn next_edit_collection_prompt_block(&self) -> Self { + match self { + ActiveRadarrBlock::EditCollectionToggleMonitored => { + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability + } + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability => { + ActiveRadarrBlock::EditCollectionSelectQualityProfile + } + ActiveRadarrBlock::EditCollectionSelectQualityProfile => { + ActiveRadarrBlock::EditCollectionRootFolderPathInput + } + ActiveRadarrBlock::EditCollectionRootFolderPathInput => { + ActiveRadarrBlock::EditCollectionToggleSearchOnAdd + } + ActiveRadarrBlock::EditCollectionToggleSearchOnAdd => { + ActiveRadarrBlock::EditCollectionConfirmPrompt + } + _ => ActiveRadarrBlock::EditCollectionToggleMonitored, + } + } + + pub fn previous_add_movie_prompt_block(&self) -> Self { match self { ActiveRadarrBlock::AddMovieSelectMonitor => ActiveRadarrBlock::AddMovieConfirmPrompt, ActiveRadarrBlock::AddMovieSelectMinimumAvailability => { @@ -410,7 +490,7 @@ impl ActiveRadarrBlock { } } - pub fn previous_edit_prompt_block(&self) -> Self { + pub fn previous_edit_movie_prompt_block(&self) -> Self { match self { ActiveRadarrBlock::EditMovieToggleMonitored => ActiveRadarrBlock::EditMovieConfirmPrompt, ActiveRadarrBlock::EditMovieSelectMinimumAvailability => { @@ -425,6 +505,30 @@ impl ActiveRadarrBlock { _ => ActiveRadarrBlock::EditMovieToggleMonitored, } } + + pub fn previous_edit_collection_prompt_block(&self) -> Self { + match self { + ActiveRadarrBlock::EditCollectionToggleMonitored => { + ActiveRadarrBlock::EditCollectionConfirmPrompt + } + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability => { + ActiveRadarrBlock::EditCollectionToggleMonitored + } + ActiveRadarrBlock::EditCollectionSelectQualityProfile => { + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability + } + ActiveRadarrBlock::EditCollectionRootFolderPathInput => { + ActiveRadarrBlock::EditCollectionSelectQualityProfile + } + ActiveRadarrBlock::EditCollectionToggleSearchOnAdd => { + ActiveRadarrBlock::EditCollectionRootFolderPathInput + } + ActiveRadarrBlock::EditCollectionConfirmPrompt => { + ActiveRadarrBlock::EditCollectionToggleSearchOnAdd + } + _ => ActiveRadarrBlock::EditCollectionToggleMonitored, + } + } } impl From for Route { @@ -538,10 +642,14 @@ impl App { self.dispatch_by_radarr_block(&active_radarr_block).await; } - if self.is_routing || self.tick_count % self.tick_until_poll == 0 { - self.refresh_metadata().await; + if self.should_refresh { self.dispatch_by_radarr_block(&active_radarr_block).await; } + + if self.is_routing || self.tick_count % self.tick_until_poll == 0 { + self.dispatch_by_radarr_block(&active_radarr_block).await; + self.refresh_metadata().await; + } } async fn refresh_metadata(&mut self) { @@ -603,6 +711,7 @@ pub mod radarr_test_utils { edit_path: "test path".to_owned().into(), edit_tags: "usenet, test".to_owned().into(), edit_monitored: Some(true), + edit_search_on_add: Some(true), file_details: "test file details".to_owned(), audio_details: "test audio details".to_owned(), video_details: "test video details".to_owned(), @@ -618,14 +727,12 @@ pub mod radarr_test_utils { .movie_releases .set_items(vec![Release::default()]); radarr_data.movie_info_tabs.index = 1; + radarr_data.monitor_list.set_items(vec![Monitor::default()]); radarr_data - .movie_monitor_list - .set_items(vec![Monitor::default()]); - radarr_data - .movie_minimum_availability_list + .minimum_availability_list .set_items(vec![MinimumAvailability::default()]); radarr_data - .movie_quality_profile_list + .quality_profile_list .set_items(vec![String::default()]); radarr_data .movie_releases_sort @@ -667,9 +774,10 @@ pub mod radarr_test_utils { } #[macro_export] - macro_rules! assert_edit_movie_reset { + macro_rules! assert_edit_media_reset { ($radarr_data:expr) => { assert!($radarr_data.edit_monitored.is_none()); + assert!($radarr_data.edit_search_on_add.is_none()); assert!($radarr_data.edit_path.text.is_empty()); assert!($radarr_data.edit_tags.text.is_empty()); }; @@ -703,14 +811,11 @@ pub mod radarr_test_utils { } #[macro_export] - macro_rules! assert_movie_preferences_selections_reset { + macro_rules! assert_preferences_selections_reset { ($radarr_data:expr) => { - assert!($radarr_data.movie_monitor_list.items.is_empty()); - assert!($radarr_data - .movie_minimum_availability_list - .items - .is_empty()); - assert!($radarr_data.movie_quality_profile_list.items.is_empty()); + assert!($radarr_data.monitor_list.items.is_empty()); + assert!($radarr_data.minimum_availability_list.items.is_empty()); + assert!($radarr_data.quality_profile_list.items.is_empty()); }; } } @@ -719,6 +824,7 @@ pub mod radarr_test_utils { mod tests { mod radarr_data_tests { use bimap::BiMap; + use chrono::{DateTime, Utc}; use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; use serde_json::Number; @@ -726,8 +832,10 @@ mod tests { use crate::app::radarr::radarr_test_utils::create_test_radarr_data; use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; - use crate::models::radarr_models::{MinimumAvailability, Monitor, Movie}; - use crate::models::{HorizontallyScrollableText, Route, StatefulTable}; + use crate::models::radarr_models::{Collection, MinimumAvailability, Monitor, Movie}; + use crate::models::HorizontallyScrollableText; + use crate::models::Route; + use crate::models::StatefulTable; #[test] fn test_from_tuple_to_route_with_context() { @@ -780,30 +888,31 @@ mod tests { } #[test] - fn test_reset_edit_movie() { + fn test_reset_add_edit_media_fields() { let mut radarr_data = RadarrData { edit_monitored: Some(true), + edit_search_on_add: Some(true), edit_path: "test path".to_owned().into(), edit_tags: "test tag".to_owned().into(), ..RadarrData::default() }; - radarr_data.reset_add_edit_movie_fields(); + radarr_data.reset_add_edit_media_fields(); - assert_edit_movie_reset!(radarr_data); + assert_edit_media_reset!(radarr_data); } #[test] - fn test_reset_movie_preferences_selections() { + fn test_reset_preferences_selections() { let mut radarr_data = create_test_radarr_data(); - radarr_data.reset_movie_preferences_selections(); + radarr_data.reset_preferences_selections(); - assert_movie_preferences_selections_reset!(radarr_data); + assert_preferences_selections_reset!(radarr_data); } #[test] - fn test_populate_movie_preferences_lists() { + fn test_populate_preferences_lists() { let mut radarr_data = RadarrData { quality_profile_map: BiMap::from_iter([ (2222, "HD - 1080p".to_owned()), @@ -812,18 +921,18 @@ mod tests { ..RadarrData::default() }; - radarr_data.populate_movie_preferences_lists(); + radarr_data.populate_preferences_lists(); assert_eq!( - radarr_data.movie_monitor_list.items, + radarr_data.monitor_list.items, Vec::from_iter(Monitor::iter()) ); assert_eq!( - radarr_data.movie_minimum_availability_list.items, + radarr_data.minimum_availability_list.items, Vec::from_iter(MinimumAvailability::iter()) ); assert_eq!( - radarr_data.movie_quality_profile_list.items, + radarr_data.quality_profile_list.items, vec!["Any".to_owned(), "HD - 1080p".to_owned()] ); } @@ -860,27 +969,236 @@ mod tests { radarr_data.populate_edit_movie_fields(); assert_eq!( - radarr_data.movie_minimum_availability_list.items, + radarr_data.minimum_availability_list.items, Vec::from_iter(MinimumAvailability::iter()) ); assert_eq!( - radarr_data - .movie_minimum_availability_list - .current_selection(), + radarr_data.minimum_availability_list.current_selection(), &MinimumAvailability::Released ); assert_eq!( - radarr_data.movie_quality_profile_list.items, + radarr_data.quality_profile_list.items, vec!["Any".to_owned(), "HD - 1080p".to_owned()] ); assert_str_eq!( - radarr_data.movie_quality_profile_list.current_selection(), + radarr_data.quality_profile_list.current_selection(), "HD - 1080p" ); assert_str_eq!(radarr_data.edit_path.text, "/nfs/movies/Test"); assert_str_eq!(radarr_data.edit_tags.text, "usenet, test"); assert_eq!(radarr_data.edit_monitored, Some(true)); } + + #[rstest] + fn test_populate_edit_collection_fields( + #[values(true, false)] test_filtered_collections: bool, + ) { + let mut radarr_data = RadarrData { + edit_path: HorizontallyScrollableText::default(), + edit_monitored: None, + edit_search_on_add: None, + quality_profile_map: BiMap::from_iter([ + (2222, "HD - 1080p".to_owned()), + (1111, "Any".to_owned()), + ]), + filtered_collections: StatefulTable::default(), + ..create_test_radarr_data() + }; + let collection = Collection { + root_folder_path: Some("/nfs/movies/Test".to_owned()), + monitored: true, + search_on_add: true, + quality_profile_id: Number::from(2222), + minimum_availability: MinimumAvailability::Released, + ..Collection::default() + }; + + if test_filtered_collections { + radarr_data.filtered_collections.set_items(vec![collection]); + } else { + radarr_data.collections.set_items(vec![collection]); + } + + radarr_data.populate_edit_collection_fields(); + + assert_eq!( + radarr_data.minimum_availability_list.items, + Vec::from_iter(MinimumAvailability::iter()) + ); + assert_eq!( + radarr_data.minimum_availability_list.current_selection(), + &MinimumAvailability::Released + ); + assert_eq!( + radarr_data.quality_profile_list.items, + vec!["Any".to_owned(), "HD - 1080p".to_owned()] + ); + assert_str_eq!( + radarr_data.quality_profile_list.current_selection(), + "HD - 1080p" + ); + assert_str_eq!(radarr_data.edit_path.text, "/nfs/movies/Test"); + assert_eq!(radarr_data.edit_monitored, Some(true)); + assert_eq!(radarr_data.edit_search_on_add, Some(true)); + } + + #[test] + fn test_radarr_data_defaults() { + let radarr_data = RadarrData::default(); + + assert_eq!(radarr_data.root_folders, Vec::new()); + assert_eq!(radarr_data.disk_space_vec, Vec::new()); + assert!(radarr_data.version.is_empty()); + assert_eq!(radarr_data.start_time, >::default()); + assert!(radarr_data.movies.items.is_empty()); + assert!(radarr_data.add_searched_movies.items.is_empty()); + assert!(radarr_data.monitor_list.items.is_empty()); + assert!(radarr_data.minimum_availability_list.items.is_empty()); + assert!(radarr_data.quality_profile_list.items.is_empty()); + assert_eq!( + radarr_data.selected_block, + ActiveRadarrBlock::AddMovieSelectMonitor + ); + assert!(radarr_data.filtered_movies.items.is_empty()); + assert!(radarr_data.downloads.items.is_empty()); + assert!(radarr_data.quality_profile_map.is_empty()); + assert!(radarr_data.tags_map.is_empty()); + assert!(radarr_data.file_details.is_empty()); + assert!(radarr_data.audio_details.is_empty()); + assert!(radarr_data.video_details.is_empty()); + assert!(radarr_data.movie_details.get_text().is_empty()); + assert!(radarr_data.movie_history.items.is_empty()); + assert!(radarr_data.movie_cast.items.is_empty()); + assert!(radarr_data.movie_crew.items.is_empty()); + assert!(radarr_data.movie_releases.items.is_empty()); + assert!(radarr_data.movie_releases_sort.items.is_empty()); + assert!(radarr_data.collections.items.is_empty()); + assert!(radarr_data.filtered_collections.items.is_empty()); + assert!(radarr_data.collection_movies.items.is_empty()); + assert!(radarr_data.prompt_confirm_action.is_none()); + assert!(radarr_data.search.text.is_empty()); + assert!(radarr_data.filter.text.is_empty()); + assert!(radarr_data.edit_path.text.is_empty()); + assert!(radarr_data.edit_tags.text.is_empty()); + assert!(radarr_data.edit_monitored.is_none()); + assert!(radarr_data.edit_search_on_add.is_none()); + assert!(radarr_data.sort_ascending.is_none()); + assert!(!radarr_data.is_searching); + assert!(!radarr_data.is_filtering); + assert!(!radarr_data.prompt_confirm); + + assert_eq!(radarr_data.main_tabs.tabs.len(), 3); + + assert_str_eq!(radarr_data.main_tabs.tabs[0].title, "Library"); + assert_eq!( + radarr_data.main_tabs.tabs[0].route, + ActiveRadarrBlock::Movies.into() + ); + assert!(radarr_data.main_tabs.tabs[0].help.is_empty()); + assert_eq!(radarr_data.main_tabs.tabs[0].contextual_help, + Some(" add | edit | search | filter | refresh | update all | details | cancel filter | delete".to_owned())); + + assert_str_eq!(radarr_data.main_tabs.tabs[1].title, "Downloads"); + assert_eq!( + radarr_data.main_tabs.tabs[1].route, + ActiveRadarrBlock::Downloads.into() + ); + assert!(radarr_data.main_tabs.tabs[1].help.is_empty()); + assert_eq!( + radarr_data.main_tabs.tabs[1].contextual_help, + Some(" refresh | delete".to_owned()) + ); + + assert_str_eq!(radarr_data.main_tabs.tabs[2].title, "Collections"); + assert_eq!( + radarr_data.main_tabs.tabs[2].route, + ActiveRadarrBlock::Collections.into() + ); + assert!(radarr_data.main_tabs.tabs[2].help.is_empty()); + assert_eq!(radarr_data.main_tabs.tabs[2].contextual_help, + Some(" search | edit | filter | refresh | update all | details | cancel filter".to_owned())); + + assert_eq!(radarr_data.movie_info_tabs.tabs.len(), 6); + + assert_str_eq!(radarr_data.movie_info_tabs.tabs[0].title, "Details"); + assert_eq!( + radarr_data.movie_info_tabs.tabs[0].route, + ActiveRadarrBlock::MovieDetails.into() + ); + assert_str_eq!( + radarr_data.movie_info_tabs.tabs[0].help, + " refresh | update | edit | auto search | close" + ); + assert!(radarr_data.movie_info_tabs.tabs[0] + .contextual_help + .is_none()); + + assert_str_eq!(radarr_data.movie_info_tabs.tabs[1].title, "History"); + assert_eq!( + radarr_data.movie_info_tabs.tabs[1].route, + ActiveRadarrBlock::MovieHistory.into() + ); + assert_str_eq!( + radarr_data.movie_info_tabs.tabs[1].help, + " refresh | update | edit | auto search | close" + ); + assert!(radarr_data.movie_info_tabs.tabs[1] + .contextual_help + .is_none()); + + assert_str_eq!(radarr_data.movie_info_tabs.tabs[2].title, "File"); + assert_eq!( + radarr_data.movie_info_tabs.tabs[2].route, + ActiveRadarrBlock::FileInfo.into() + ); + assert_str_eq!( + radarr_data.movie_info_tabs.tabs[2].help, + " refresh | update | edit | auto search | close" + ); + assert!(radarr_data.movie_info_tabs.tabs[2] + .contextual_help + .is_none()); + + assert_str_eq!(radarr_data.movie_info_tabs.tabs[3].title, "Cast"); + assert_eq!( + radarr_data.movie_info_tabs.tabs[3].route, + ActiveRadarrBlock::Cast.into() + ); + assert_str_eq!( + radarr_data.movie_info_tabs.tabs[3].help, + " refresh | update | edit | auto search | close" + ); + assert!(radarr_data.movie_info_tabs.tabs[3] + .contextual_help + .is_none()); + + assert_str_eq!(radarr_data.movie_info_tabs.tabs[4].title, "Crew"); + assert_eq!( + radarr_data.movie_info_tabs.tabs[4].route, + ActiveRadarrBlock::Crew.into() + ); + assert_str_eq!( + radarr_data.movie_info_tabs.tabs[4].help, + " refresh | update | edit | auto search | close" + ); + assert!(radarr_data.movie_info_tabs.tabs[4] + .contextual_help + .is_none()); + + assert_str_eq!(radarr_data.movie_info_tabs.tabs[5].title, "Manual Search"); + assert_eq!( + radarr_data.movie_info_tabs.tabs[5].route, + ActiveRadarrBlock::ManualSearch.into() + ); + assert_str_eq!( + radarr_data.movie_info_tabs.tabs[5].help, + " refresh | update | edit | sort | auto search | close" + ); + assert_eq!( + radarr_data.movie_info_tabs.tabs[5].contextual_help, + Some(" details".to_owned()) + ); + } } mod active_radarr_block_tests { @@ -889,128 +1207,215 @@ mod tests { use crate::app::radarr::ActiveRadarrBlock; #[test] - fn test_next_add_prompt_block() { - let active_block = ActiveRadarrBlock::AddMovieSelectMonitor.next_add_prompt_block(); + fn test_next_add_movie_prompt_block() { + let active_block = ActiveRadarrBlock::AddMovieSelectMonitor.next_add_movie_prompt_block(); assert_eq!( active_block, ActiveRadarrBlock::AddMovieSelectMinimumAvailability ); - let active_block = active_block.next_add_prompt_block(); + let active_block = active_block.next_add_movie_prompt_block(); assert_eq!( active_block, ActiveRadarrBlock::AddMovieSelectQualityProfile ); - let active_block = active_block.next_add_prompt_block(); + let active_block = active_block.next_add_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::AddMovieTagsInput); - let active_block = active_block.next_add_prompt_block(); + let active_block = active_block.next_add_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::AddMovieConfirmPrompt); - let active_block = active_block.next_add_prompt_block(); + let active_block = active_block.next_add_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::AddMovieSelectMonitor); } #[test] - fn test_next_edit_prompt_block() { - let active_block = ActiveRadarrBlock::EditMovieToggleMonitored.next_edit_prompt_block(); + fn test_next_edit_movie_prompt_block() { + let active_block = ActiveRadarrBlock::EditMovieToggleMonitored.next_edit_movie_prompt_block(); assert_eq!( active_block, ActiveRadarrBlock::EditMovieSelectMinimumAvailability ); - let active_block = active_block.next_edit_prompt_block(); + let active_block = active_block.next_edit_movie_prompt_block(); assert_eq!( active_block, ActiveRadarrBlock::EditMovieSelectQualityProfile ); - let active_block = active_block.next_edit_prompt_block(); + let active_block = active_block.next_edit_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::EditMoviePathInput); - let active_block = active_block.next_edit_prompt_block(); + let active_block = active_block.next_edit_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::EditMovieTagsInput); - let active_block = active_block.next_edit_prompt_block(); + let active_block = active_block.next_edit_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::EditMovieConfirmPrompt); - let active_block = active_block.next_edit_prompt_block(); + let active_block = active_block.next_edit_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::EditMovieToggleMonitored); } #[test] - fn test_previous_add_prompt_block() { - let active_block = ActiveRadarrBlock::AddMovieSelectMonitor.previous_add_prompt_block(); + fn test_next_edit_collection_prompt_block() { + let active_block = + ActiveRadarrBlock::EditCollectionToggleMonitored.next_edit_collection_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability + ); + + let active_block = active_block.next_edit_collection_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditCollectionSelectQualityProfile + ); + + let active_block = active_block.next_edit_collection_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditCollectionRootFolderPathInput + ); + + let active_block = active_block.next_edit_collection_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditCollectionToggleSearchOnAdd + ); + + let active_block = active_block.next_edit_collection_prompt_block(); + + assert_eq!(active_block, ActiveRadarrBlock::EditCollectionConfirmPrompt); + + let active_block = active_block.next_edit_collection_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditCollectionToggleMonitored + ); + } + + #[test] + fn test_previous_add_movie_prompt_block() { + let active_block = ActiveRadarrBlock::AddMovieSelectMonitor.previous_add_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::AddMovieConfirmPrompt); - let active_block = active_block.previous_add_prompt_block(); + let active_block = active_block.previous_add_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::AddMovieTagsInput); - let active_block = active_block.previous_add_prompt_block(); + let active_block = active_block.previous_add_movie_prompt_block(); assert_eq!( active_block, ActiveRadarrBlock::AddMovieSelectQualityProfile ); - let active_block = active_block.previous_add_prompt_block(); + let active_block = active_block.previous_add_movie_prompt_block(); assert_eq!( active_block, ActiveRadarrBlock::AddMovieSelectMinimumAvailability ); - let active_block = active_block.previous_add_prompt_block(); + let active_block = active_block.previous_add_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::AddMovieSelectMonitor); } #[test] - fn test_previous_edit_prompt_block() { - let active_block = ActiveRadarrBlock::EditMovieToggleMonitored.previous_edit_prompt_block(); + fn test_previous_edit_movie_prompt_block() { + let active_block = + ActiveRadarrBlock::EditMovieToggleMonitored.previous_edit_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::EditMovieConfirmPrompt); - let active_block = active_block.previous_edit_prompt_block(); + let active_block = active_block.previous_edit_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::EditMovieTagsInput); - let active_block = active_block.previous_edit_prompt_block(); + let active_block = active_block.previous_edit_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::EditMoviePathInput); - let active_block = active_block.previous_edit_prompt_block(); + let active_block = active_block.previous_edit_movie_prompt_block(); assert_eq!( active_block, ActiveRadarrBlock::EditMovieSelectQualityProfile ); - let active_block = active_block.previous_edit_prompt_block(); + let active_block = active_block.previous_edit_movie_prompt_block(); assert_eq!( active_block, ActiveRadarrBlock::EditMovieSelectMinimumAvailability ); - let active_block = active_block.previous_edit_prompt_block(); + let active_block = active_block.previous_edit_movie_prompt_block(); assert_eq!(active_block, ActiveRadarrBlock::EditMovieToggleMonitored); } + + #[test] + fn test_previous_edit_collection_prompt_block() { + let active_block = + ActiveRadarrBlock::EditCollectionToggleMonitored.previous_edit_collection_prompt_block(); + + assert_eq!(active_block, ActiveRadarrBlock::EditCollectionConfirmPrompt); + + let active_block = active_block.previous_edit_collection_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditCollectionToggleSearchOnAdd + ); + + let active_block = active_block.previous_edit_collection_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditCollectionRootFolderPathInput + ); + + let active_block = active_block.previous_edit_collection_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditCollectionSelectQualityProfile + ); + + let active_block = active_block.previous_edit_collection_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability + ); + + let active_block = active_block.previous_edit_collection_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditCollectionToggleMonitored + ); + } } mod radarr_tests { @@ -1430,6 +1835,10 @@ mod tests { .radarr_on_tick(ActiveRadarrBlock::Downloads, false) .await; + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDownloads.into() + ); assert_eq!( sync_network_rx.recv().await.unwrap(), RadarrEvent::GetQualityProfiles.into() @@ -1446,6 +1855,24 @@ mod tests { assert!(!app.data.radarr_data.prompt_confirm); } + #[tokio::test] + async fn test_radarr_on_tick_should_refresh() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.should_refresh = true; + + app + .radarr_on_tick(ActiveRadarrBlock::Downloads, false) + .await; + + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDownloads.into() + ); + assert!(app.is_loading); + assert!(app.should_refresh); + assert!(!app.data.radarr_data.prompt_confirm); + } + #[tokio::test] async fn test_radarr_on_tick_network_tick_frequency() { let (mut app, mut sync_network_rx) = construct_app_unit(); @@ -1456,6 +1883,10 @@ mod tests { .radarr_on_tick(ActiveRadarrBlock::Downloads, false) .await; + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetDownloads.into() + ); assert_eq!( sync_network_rx.recv().await.unwrap(), RadarrEvent::GetQualityProfiles.into() diff --git a/src/handlers/radarr_handlers/add_movie_handler.rs b/src/handlers/radarr_handlers/add_movie_handler.rs index 3bc220a..2acd85e 100644 --- a/src/handlers/radarr_handlers/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/add_movie_handler.rs @@ -37,20 +37,17 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { self.app.data.radarr_data.add_searched_movies.scroll_up() } ActiveRadarrBlock::AddMovieSelectMonitor => { - self.app.data.radarr_data.movie_monitor_list.scroll_up() + self.app.data.radarr_data.monitor_list.scroll_up() } ActiveRadarrBlock::AddMovieSelectMinimumAvailability => self .app .data .radarr_data - .movie_minimum_availability_list - .scroll_up(), - ActiveRadarrBlock::AddMovieSelectQualityProfile => self - .app - .data - .radarr_data - .movie_quality_profile_list + .minimum_availability_list .scroll_up(), + ActiveRadarrBlock::AddMovieSelectQualityProfile => { + self.app.data.radarr_data.quality_profile_list.scroll_up() + } ActiveRadarrBlock::AddMoviePrompt => { self.app.data.radarr_data.selected_block = self .app @@ -58,7 +55,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { .radarr_data .selected_block .clone() - .previous_add_prompt_block() + .previous_add_movie_prompt_block() } _ => (), } @@ -70,27 +67,24 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { self.app.data.radarr_data.add_searched_movies.scroll_down() } ActiveRadarrBlock::AddMovieSelectMonitor => { - self.app.data.radarr_data.movie_monitor_list.scroll_down() + self.app.data.radarr_data.monitor_list.scroll_down() } ActiveRadarrBlock::AddMovieSelectMinimumAvailability => self .app .data .radarr_data - .movie_minimum_availability_list - .scroll_down(), - ActiveRadarrBlock::AddMovieSelectQualityProfile => self - .app - .data - .radarr_data - .movie_quality_profile_list + .minimum_availability_list .scroll_down(), + ActiveRadarrBlock::AddMovieSelectQualityProfile => { + self.app.data.radarr_data.quality_profile_list.scroll_down() + } ActiveRadarrBlock::AddMoviePrompt => { self.app.data.radarr_data.selected_block = self .app .data .radarr_data .selected_block - .next_add_prompt_block() + .next_add_movie_prompt_block() } _ => (), } @@ -105,19 +99,19 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { .add_searched_movies .scroll_to_top(), ActiveRadarrBlock::AddMovieSelectMonitor => { - self.app.data.radarr_data.movie_monitor_list.scroll_to_top() + self.app.data.radarr_data.monitor_list.scroll_to_top() } ActiveRadarrBlock::AddMovieSelectMinimumAvailability => self .app .data .radarr_data - .movie_minimum_availability_list + .minimum_availability_list .scroll_to_top(), ActiveRadarrBlock::AddMovieSelectQualityProfile => self .app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .scroll_to_top(), ActiveRadarrBlock::AddMovieSearchInput => self.app.data.radarr_data.search.scroll_home(), ActiveRadarrBlock::AddMovieTagsInput => self.app.data.radarr_data.edit_tags.scroll_home(), @@ -133,23 +127,20 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { .radarr_data .add_searched_movies .scroll_to_bottom(), - ActiveRadarrBlock::AddMovieSelectMonitor => self - .app - .data - .radarr_data - .movie_monitor_list - .scroll_to_bottom(), + ActiveRadarrBlock::AddMovieSelectMonitor => { + self.app.data.radarr_data.monitor_list.scroll_to_bottom() + } ActiveRadarrBlock::AddMovieSelectMinimumAvailability => self .app .data .radarr_data - .movie_minimum_availability_list + .minimum_availability_list .scroll_to_bottom(), ActiveRadarrBlock::AddMovieSelectQualityProfile => self .app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .scroll_to_bottom(), ActiveRadarrBlock::AddMovieSearchInput => self.app.data.radarr_data.search.reset_offset(), ActiveRadarrBlock::AddMovieTagsInput => self.app.data.radarr_data.edit_tags.reset_offset(), @@ -214,7 +205,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { self .app .push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); - self.app.data.radarr_data.populate_movie_preferences_lists(); + self.app.data.radarr_data.populate_preferences_lists(); self.app.data.radarr_data.selected_block = ActiveRadarrBlock::AddMovieSelectMonitor; } } @@ -265,7 +256,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { } ActiveRadarrBlock::AddMoviePrompt => { self.app.pop_navigation_stack(); - self.app.data.radarr_data.reset_add_edit_movie_fields(); + self.app.data.radarr_data.reset_add_edit_media_fields(); self.app.data.radarr_data.prompt_confirm = false; } ActiveRadarrBlock::AddMovieSelectMonitor @@ -331,16 +322,16 @@ mod tests { test_add_movie_select_monitor_scroll, AddMovieHandler, Monitor, - movie_monitor_list, + monitor_list, ActiveRadarrBlock::AddMovieSelectMonitor, None ); test_enum_scroll!( - test_add_movie_select_minimuum_availability_scroll, + test_add_movie_select_minimum_availability_scroll, AddMovieHandler, MinimumAvailability, - movie_minimum_availability_list, + minimum_availability_list, ActiveRadarrBlock::AddMovieSelectMinimumAvailability, None ); @@ -348,7 +339,7 @@ mod tests { test_iterable_scroll!( test_add_movie_select_quality_profile_scroll, AddMovieHandler, - movie_quality_profile_list, + quality_profile_list, ActiveRadarrBlock::AddMovieSelectQualityProfile, None ); @@ -399,16 +390,16 @@ mod tests { test_add_movie_select_monitor_home_end, AddMovieHandler, Monitor, - movie_monitor_list, + monitor_list, ActiveRadarrBlock::AddMovieSelectMonitor, None ); test_enum_home_and_end!( - test_add_movie_select_minimuum_availability_home_end, + test_add_movie_select_minimum_availability_home_end, AddMovieHandler, MinimumAvailability, - movie_minimum_availability_list, + minimum_availability_list, ActiveRadarrBlock::AddMovieSelectMinimumAvailability, None ); @@ -416,7 +407,7 @@ mod tests { test_iterable_home_and_end!( test_add_movie_select_quality_profile_scroll, AddMovieHandler, - movie_quality_profile_list, + quality_profile_list, ActiveRadarrBlock::AddMovieSelectQualityProfile, None ); @@ -539,24 +530,19 @@ mod tests { app.data.radarr_data.selected_block, ActiveRadarrBlock::AddMovieSelectMonitor ); - assert!(!app.data.radarr_data.movie_monitor_list.items.is_empty()); + assert!(!app.data.radarr_data.monitor_list.items.is_empty()); assert!(!app .data .radarr_data - .movie_minimum_availability_list - .items - .is_empty()); - assert!(!app - .data - .radarr_data - .movie_quality_profile_list + .minimum_availability_list .items .is_empty()); + assert!(!app.data.radarr_data.quality_profile_list.items.is_empty()); assert_str_eq!( app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .current_selection(), "A - Test 1" ); @@ -728,7 +714,7 @@ mod tests { use crate::app::radarr::radarr_test_utils::create_test_radarr_data; use crate::{ - assert_edit_movie_reset, assert_movie_preferences_selections_reset, assert_search_reset, + assert_edit_media_reset, assert_preferences_selections_reset, assert_search_reset, simple_stateful_iterable_vec, }; @@ -850,8 +836,8 @@ mod tests { app.get_current_route(), &ActiveRadarrBlock::AddMovieSearchResults.into() ); - assert_movie_preferences_selections_reset!(app.data.radarr_data); - assert_edit_movie_reset!(app.data.radarr_data); + assert_preferences_selections_reset!(app.data.radarr_data); + assert_edit_media_reset!(app.data.radarr_data); } #[test] diff --git a/src/handlers/radarr_handlers/collection_details_handler.rs b/src/handlers/radarr_handlers/collection_details_handler.rs index ddbe186..ec9d7e0 100644 --- a/src/handlers/radarr_handlers/collection_details_handler.rs +++ b/src/handlers/radarr_handlers/collection_details_handler.rs @@ -1,3 +1,4 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::ActiveRadarrBlock; use crate::app::App; use crate::event::Key; @@ -95,7 +96,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for CollectionDetailsHandler<'a> .into(), ); self.app.data.radarr_data.selected_block = ActiveRadarrBlock::EditMovieToggleMonitored; - self.app.data.radarr_data.populate_movie_preferences_lists(); + self.app.data.radarr_data.populate_preferences_lists(); } } } @@ -111,7 +112,21 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for CollectionDetailsHandler<'a> } } - fn handle_char_key_event(&mut self) {} + fn handle_char_key_event(&mut self) { + if *self.active_radarr_block == ActiveRadarrBlock::CollectionDetails + && *self.key == DEFAULT_KEYBINDINGS.edit.key + { + self.app.push_navigation_stack( + ( + ActiveRadarrBlock::EditCollectionPrompt, + Some(*self.active_radarr_block), + ) + .into(), + ); + self.app.data.radarr_data.populate_edit_collection_fields(); + self.app.data.radarr_data.selected_block = ActiveRadarrBlock::EditCollectionToggleMonitored; + } + } } #[cfg(test)] @@ -201,7 +216,7 @@ mod tests { ) .into() ); - assert!(!app.data.radarr_data.movie_monitor_list.items.is_empty()); + assert!(!app.data.radarr_data.monitor_list.items.is_empty()); assert_eq!( app.data.radarr_data.selected_block, ActiveRadarrBlock::EditMovieToggleMonitored @@ -209,20 +224,15 @@ mod tests { assert!(!app .data .radarr_data - .movie_minimum_availability_list - .items - .is_empty()); - assert!(!app - .data - .radarr_data - .movie_quality_profile_list + .minimum_availability_list .items .is_empty()); + assert!(!app.data.radarr_data.quality_profile_list.items.is_empty()); assert_str_eq!( app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .current_selection(), "A - Test 1" ); @@ -310,4 +320,29 @@ mod tests { ); } } + + mod test_handle_key_char { + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; + use serde_json::Number; + use strum::IntoEnumIterator; + + use crate::app::radarr::radarr_test_utils::create_test_radarr_data; + use crate::app::radarr::RadarrData; + use crate::models::radarr_models::{Collection, MinimumAvailability}; + use crate::models::HorizontallyScrollableText; + use crate::models::StatefulTable; + use crate::test_edit_collection_key; + + use super::*; + + #[test] + fn test_edit_key() { + test_edit_collection_key!( + CollectionDetailsHandler, + ActiveRadarrBlock::CollectionDetails, + ActiveRadarrBlock::CollectionDetails + ); + } + } } diff --git a/src/handlers/radarr_handlers/edit_collection_handler.rs b/src/handlers/radarr_handlers/edit_collection_handler.rs new file mode 100644 index 0000000..f7893e1 --- /dev/null +++ b/src/handlers/radarr_handlers/edit_collection_handler.rs @@ -0,0 +1,693 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::radarr::ActiveRadarrBlock; +use crate::app::App; +use crate::event::Key; +use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::Scrollable; +use crate::network::radarr_network::RadarrEvent; +use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; + +pub(super) struct EditCollectionHandler<'a> { + key: &'a Key, + app: &'a mut App, + active_radarr_block: &'a ActiveRadarrBlock, + context: &'a Option, +} + +impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditCollectionHandler<'a> { + fn with( + key: &'a Key, + app: &'a mut App, + active_block: &'a ActiveRadarrBlock, + context: &'a Option, + ) -> EditCollectionHandler<'a> { + EditCollectionHandler { + key, + app, + active_radarr_block: active_block, + context, + } + } + + fn get_key(&self) -> &Key { + self.key + } + + fn handle_scroll_up(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability => self + .app + .data + .radarr_data + .minimum_availability_list + .scroll_up(), + ActiveRadarrBlock::EditCollectionSelectQualityProfile => { + self.app.data.radarr_data.quality_profile_list.scroll_up() + } + ActiveRadarrBlock::EditCollectionPrompt => { + self.app.data.radarr_data.selected_block = self + .app + .data + .radarr_data + .selected_block + .clone() + .previous_edit_collection_prompt_block() + } + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability => self + .app + .data + .radarr_data + .minimum_availability_list + .scroll_down(), + ActiveRadarrBlock::EditCollectionSelectQualityProfile => { + self.app.data.radarr_data.quality_profile_list.scroll_down() + } + ActiveRadarrBlock::EditCollectionPrompt => { + self.app.data.radarr_data.selected_block = self + .app + .data + .radarr_data + .selected_block + .next_edit_collection_prompt_block() + } + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability => self + .app + .data + .radarr_data + .minimum_availability_list + .scroll_to_top(), + ActiveRadarrBlock::EditCollectionSelectQualityProfile => self + .app + .data + .radarr_data + .quality_profile_list + .scroll_to_top(), + ActiveRadarrBlock::EditCollectionRootFolderPathInput => { + self.app.data.radarr_data.edit_path.scroll_home() + } + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability => self + .app + .data + .radarr_data + .minimum_availability_list + .scroll_to_bottom(), + ActiveRadarrBlock::EditCollectionSelectQualityProfile => self + .app + .data + .radarr_data + .quality_profile_list + .scroll_to_bottom(), + ActiveRadarrBlock::EditCollectionRootFolderPathInput => { + self.app.data.radarr_data.edit_path.reset_offset() + } + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::EditCollectionPrompt => handle_prompt_toggle(self.app, self.key), + ActiveRadarrBlock::EditCollectionRootFolderPathInput => { + handle_text_box_left_right_keys!(self, self.key, self.app.data.radarr_data.edit_path) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::EditCollectionPrompt => match self.app.data.radarr_data.selected_block { + ActiveRadarrBlock::EditCollectionConfirmPrompt => { + if self.app.data.radarr_data.prompt_confirm { + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditCollection); + self.app.pop_navigation_stack(); + self.app.should_refresh = true; + } else { + self.app.pop_navigation_stack(); + } + } + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability + | ActiveRadarrBlock::EditCollectionSelectQualityProfile => self + .app + .push_navigation_stack((self.app.data.radarr_data.selected_block, *self.context).into()), + ActiveRadarrBlock::EditCollectionRootFolderPathInput => { + self.app.push_navigation_stack( + (self.app.data.radarr_data.selected_block, *self.context).into(), + ); + self.app.should_ignore_quit_key = true; + } + ActiveRadarrBlock::EditCollectionToggleMonitored => { + self.app.data.radarr_data.edit_monitored = + Some(!self.app.data.radarr_data.edit_monitored.unwrap_or_default()) + } + ActiveRadarrBlock::EditCollectionToggleSearchOnAdd => { + self.app.data.radarr_data.edit_search_on_add = Some( + !self + .app + .data + .radarr_data + .edit_search_on_add + .unwrap_or_default(), + ) + } + _ => (), + }, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability + | ActiveRadarrBlock::EditCollectionSelectQualityProfile => self.app.pop_navigation_stack(), + ActiveRadarrBlock::EditCollectionRootFolderPathInput => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::EditCollectionRootFolderPathInput => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + } + ActiveRadarrBlock::EditCollectionPrompt => { + self.app.pop_navigation_stack(); + self.app.data.radarr_data.reset_add_edit_media_fields(); + self.app.data.radarr_data.prompt_confirm = false; + } + ActiveRadarrBlock::EditCollectionToggleMonitored + | ActiveRadarrBlock::EditCollectionToggleSearchOnAdd + | ActiveRadarrBlock::EditCollectionSelectMinimumAvailability + | ActiveRadarrBlock::EditCollectionSelectQualityProfile => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + if self.active_radarr_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput { + handle_text_box_keys!(self, key, self.app.data.radarr_data.edit_path) + } + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_str_eq; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::radarr::ActiveRadarrBlock; + use crate::app::App; + use crate::event::Key; + use crate::handlers::radarr_handlers::edit_collection_handler::EditCollectionHandler; + use crate::handlers::KeyEventHandler; + use crate::models::radarr_models::MinimumAvailability; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::{test_enum_scroll, test_iterable_scroll}; + + use super::*; + + test_enum_scroll!( + test_edit_collection_select_minimum_availability_scroll, + EditCollectionHandler, + MinimumAvailability, + minimum_availability_list, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + None + ); + + test_iterable_scroll!( + test_edit_collection_select_quality_profile_scroll, + EditCollectionHandler, + quality_profile_list, + ActiveRadarrBlock::EditCollectionSelectQualityProfile, + None + ); + + #[rstest] + fn test_edit_collection_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.data.radarr_data.selected_block = + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability; + + EditCollectionHandler::with( + &key, + &mut app, + &ActiveRadarrBlock::EditCollectionPrompt, + &None, + ) + .handle(); + + if key == Key::Up { + assert_eq!( + app.data.radarr_data.selected_block, + ActiveRadarrBlock::EditCollectionToggleMonitored + ); + } else { + assert_eq!( + app.data.radarr_data.selected_block, + ActiveRadarrBlock::EditCollectionSelectQualityProfile + ); + } + } + } + + mod test_handle_home_end { + use strum::IntoEnumIterator; + + use crate::{test_enum_home_and_end, test_iterable_home_and_end, test_text_box_home_end_keys}; + + use super::*; + + test_enum_home_and_end!( + test_edit_collection_select_minimum_availability_home_end, + EditCollectionHandler, + MinimumAvailability, + minimum_availability_list, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + None + ); + + test_iterable_home_and_end!( + test_edit_collection_select_quality_profile_scroll, + EditCollectionHandler, + quality_profile_list, + ActiveRadarrBlock::EditCollectionSelectQualityProfile, + None + ); + + #[test] + fn test_edit_collection_root_folder_path_input_home_end_keys() { + test_text_box_home_end_keys!( + EditCollectionHandler, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + edit_path + ); + } + } + + mod test_handle_left_right_action { + use rstest::rstest; + + use crate::test_text_box_left_right_keys; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::default(); + + EditCollectionHandler::with( + &key, + &mut app, + &ActiveRadarrBlock::EditCollectionPrompt, + &None, + ) + .handle(); + + assert!(app.data.radarr_data.prompt_confirm); + + EditCollectionHandler::with( + &key, + &mut app, + &ActiveRadarrBlock::EditCollectionPrompt, + &None, + ) + .handle(); + + assert!(!app.data.radarr_data.prompt_confirm); + } + + #[test] + fn test_edit_collection_root_folder_path_input_left_right_keys() { + test_text_box_left_right_keys!( + EditCollectionHandler, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + edit_path + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::models::Route; + use crate::network::radarr_network::RadarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_edit_collection_root_folder_path_input_submit() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.data.radarr_data.edit_path = "Test Path".to_owned().into(); + app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditCollectionRootFolderPathInput.into()); + + EditCollectionHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditCollectionRootFolderPathInput, + &None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app.data.radarr_data.edit_path.text.is_empty()); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditCollectionPrompt.into() + ); + } + + #[test] + fn test_edit_collection_prompt_prompt_decline_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); + app.data.radarr_data.selected_block = ActiveRadarrBlock::EditCollectionConfirmPrompt; + + EditCollectionHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditCollectionPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Collections.into() + ); + assert_eq!(app.data.radarr_data.prompt_confirm_action, None); + } + + #[test] + fn test_edit_collection_confirm_prompt_prompt_confirmation_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); + app.data.radarr_data.prompt_confirm = true; + app.data.radarr_data.selected_block = ActiveRadarrBlock::EditCollectionConfirmPrompt; + + EditCollectionHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditCollectionPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Collections.into() + ); + assert_eq!( + app.data.radarr_data.prompt_confirm_action, + Some(RadarrEvent::EditCollection) + ); + assert!(app.should_refresh); + } + + #[test] + fn test_edit_collection_toggle_monitored_submit() { + let current_route = Route::from(( + ActiveRadarrBlock::EditCollectionPrompt, + Some(ActiveRadarrBlock::Collections), + )); + let mut app = App::default(); + app.data.radarr_data.selected_block = ActiveRadarrBlock::EditCollectionToggleMonitored; + app.push_navigation_stack(current_route); + + EditCollectionHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditCollectionPrompt, + &Some(ActiveRadarrBlock::Collections), + ) + .handle(); + + assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.data.radarr_data.edit_monitored, Some(true)); + + EditCollectionHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditCollectionPrompt, + &Some(ActiveRadarrBlock::Collections), + ) + .handle(); + + assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.data.radarr_data.edit_monitored, Some(false)); + } + + #[test] + fn test_edit_collection_toggle_search_on_add_submit() { + let current_route = Route::from(( + ActiveRadarrBlock::EditCollectionPrompt, + Some(ActiveRadarrBlock::Collections), + )); + let mut app = App::default(); + app.data.radarr_data.selected_block = ActiveRadarrBlock::EditCollectionToggleSearchOnAdd; + app.push_navigation_stack(current_route); + + EditCollectionHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditCollectionPrompt, + &Some(ActiveRadarrBlock::Collections), + ) + .handle(); + + assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.data.radarr_data.edit_search_on_add, Some(true)); + + EditCollectionHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditCollectionPrompt, + &Some(ActiveRadarrBlock::Collections), + ) + .handle(); + + assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.data.radarr_data.edit_search_on_add, Some(false)); + } + + #[rstest] + fn test_edit_collection_prompt_selected_block_submit( + #[values( + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + ActiveRadarrBlock::EditCollectionSelectQualityProfile, + ActiveRadarrBlock::EditCollectionRootFolderPathInput + )] + selected_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack( + ( + ActiveRadarrBlock::EditCollectionPrompt, + Some(ActiveRadarrBlock::Collections), + ) + .into(), + ); + app.data.radarr_data.selected_block = selected_block; + + EditCollectionHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditCollectionPrompt, + &Some(ActiveRadarrBlock::Collections), + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &(selected_block, Some(ActiveRadarrBlock::Collections)).into() + ); + assert_eq!(app.data.radarr_data.prompt_confirm_action, None); + + if selected_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput { + assert!(app.should_ignore_quit_key); + } + } + + #[rstest] + fn test_edit_collection_prompt_selecting_preferences_blocks_submit( + #[values( + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + ActiveRadarrBlock::EditCollectionSelectQualityProfile, + ActiveRadarrBlock::EditCollectionRootFolderPathInput + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); + app.push_navigation_stack(active_radarr_block.into()); + + EditCollectionHandler::with( + &SUBMIT_KEY, + &mut app, + &active_radarr_block, + &Some(ActiveRadarrBlock::Collections), + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditCollectionPrompt.into() + ); + + if active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput { + assert!(!app.should_ignore_quit_key); + } + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::app::radarr::radarr_test_utils::create_test_radarr_data; + use crate::{assert_edit_media_reset, assert_preferences_selections_reset}; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_edit_collection_root_folder_path_input_esc() { + let mut app = App::default(); + app.data.radarr_data = create_test_radarr_data(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditCollectionRootFolderPathInput.into()); + + EditCollectionHandler::with( + &ESC_KEY, + &mut app, + &ActiveRadarrBlock::EditCollectionRootFolderPathInput, + &None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditCollectionPrompt.into() + ); + } + + #[test] + fn test_edit_collection_prompt_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditCollectionPrompt.into()); + app.data.radarr_data = create_test_radarr_data(); + + EditCollectionHandler::with( + &ESC_KEY, + &mut app, + &ActiveRadarrBlock::EditCollectionPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Collections.into() + ); + let radarr_data = &app.data.radarr_data; + + assert_preferences_selections_reset!(radarr_data); + assert_edit_media_reset!(radarr_data); + assert!(!radarr_data.prompt_confirm); + } + + #[rstest] + fn test_edit_collection_esc( + #[values( + ActiveRadarrBlock::EditCollectionToggleMonitored, + ActiveRadarrBlock::EditCollectionToggleSearchOnAdd, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + ActiveRadarrBlock::EditCollectionSelectQualityProfile + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.data.radarr_data = create_test_radarr_data(); + app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); + app.push_navigation_stack(active_radarr_block.into()); + + EditCollectionHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Collections.into() + ); + } + } + + mod test_handle_key_char { + use super::*; + + #[test] + fn test_edit_collection_root_folder_path_input_backspace() { + let mut app = App::default(); + app.data.radarr_data.edit_path = "Test".to_owned().into(); + + EditCollectionHandler::with( + &DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + &ActiveRadarrBlock::EditCollectionRootFolderPathInput, + &None, + ) + .handle(); + + assert_str_eq!(app.data.radarr_data.edit_path.text, "Tes"); + } + + #[test] + fn test_edit_collection_root_folder_path_input_char_key() { + let mut app = App::default(); + + EditCollectionHandler::with( + &Key::Char('h'), + &mut app, + &ActiveRadarrBlock::EditCollectionRootFolderPathInput, + &None, + ) + .handle(); + + assert_str_eq!(app.data.radarr_data.edit_path.text, "h"); + } + } +} diff --git a/src/handlers/radarr_handlers/edit_movie_handler.rs b/src/handlers/radarr_handlers/edit_movie_handler.rs index c972d37..cd80a46 100644 --- a/src/handlers/radarr_handlers/edit_movie_handler.rs +++ b/src/handlers/radarr_handlers/edit_movie_handler.rs @@ -39,14 +39,11 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditMovieHandler<'a> { .app .data .radarr_data - .movie_minimum_availability_list - .scroll_up(), - ActiveRadarrBlock::EditMovieSelectQualityProfile => self - .app - .data - .radarr_data - .movie_quality_profile_list + .minimum_availability_list .scroll_up(), + ActiveRadarrBlock::EditMovieSelectQualityProfile => { + self.app.data.radarr_data.quality_profile_list.scroll_up() + } ActiveRadarrBlock::EditMoviePrompt => { self.app.data.radarr_data.selected_block = self .app @@ -54,7 +51,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditMovieHandler<'a> { .radarr_data .selected_block .clone() - .previous_edit_prompt_block() + .previous_edit_movie_prompt_block() } _ => (), } @@ -66,21 +63,18 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditMovieHandler<'a> { .app .data .radarr_data - .movie_minimum_availability_list - .scroll_down(), - ActiveRadarrBlock::EditMovieSelectQualityProfile => self - .app - .data - .radarr_data - .movie_quality_profile_list + .minimum_availability_list .scroll_down(), + ActiveRadarrBlock::EditMovieSelectQualityProfile => { + self.app.data.radarr_data.quality_profile_list.scroll_down() + } ActiveRadarrBlock::EditMoviePrompt => { self.app.data.radarr_data.selected_block = self .app .data .radarr_data .selected_block - .next_edit_prompt_block() + .next_edit_movie_prompt_block() } _ => (), } @@ -92,13 +86,13 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditMovieHandler<'a> { .app .data .radarr_data - .movie_minimum_availability_list + .minimum_availability_list .scroll_to_top(), ActiveRadarrBlock::EditMovieSelectQualityProfile => self .app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .scroll_to_top(), ActiveRadarrBlock::EditMoviePathInput => self.app.data.radarr_data.edit_path.scroll_home(), ActiveRadarrBlock::EditMovieTagsInput => self.app.data.radarr_data.edit_tags.scroll_home(), @@ -112,13 +106,13 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditMovieHandler<'a> { .app .data .radarr_data - .movie_minimum_availability_list + .minimum_availability_list .scroll_to_bottom(), ActiveRadarrBlock::EditMovieSelectQualityProfile => self .app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .scroll_to_bottom(), ActiveRadarrBlock::EditMoviePathInput => self.app.data.radarr_data.edit_path.reset_offset(), ActiveRadarrBlock::EditMovieTagsInput => self.app.data.radarr_data.edit_tags.reset_offset(), @@ -148,6 +142,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditMovieHandler<'a> { if self.app.data.radarr_data.prompt_confirm { self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie); self.app.pop_navigation_stack(); + self.app.should_refresh = true; } else { self.app.pop_navigation_stack(); } @@ -186,7 +181,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditMovieHandler<'a> { } ActiveRadarrBlock::EditMoviePrompt => { self.app.pop_navigation_stack(); - self.app.data.radarr_data.reset_add_edit_movie_fields(); + self.app.data.radarr_data.reset_add_edit_media_fields(); self.app.data.radarr_data.prompt_confirm = false; } ActiveRadarrBlock::EditMovieToggleMonitored @@ -232,10 +227,10 @@ mod tests { use super::*; test_enum_scroll!( - test_edit_movie_select_minimuum_availability_scroll, + test_edit_movie_select_minimum_availability_scroll, EditMovieHandler, MinimumAvailability, - movie_minimum_availability_list, + minimum_availability_list, ActiveRadarrBlock::EditMovieSelectMinimumAvailability, None ); @@ -243,7 +238,7 @@ mod tests { test_iterable_scroll!( test_edit_movie_select_quality_profile_scroll, EditMovieHandler, - movie_quality_profile_list, + quality_profile_list, ActiveRadarrBlock::EditMovieSelectQualityProfile, None ); @@ -277,10 +272,10 @@ mod tests { use super::*; test_enum_home_and_end!( - test_edit_movie_select_minimuum_availability_home_end, + test_edit_movie_select_minimum_availability_home_end, EditMovieHandler, MinimumAvailability, - movie_minimum_availability_list, + minimum_availability_list, ActiveRadarrBlock::EditMovieSelectMinimumAvailability, None ); @@ -288,7 +283,7 @@ mod tests { test_iterable_home_and_end!( test_edit_movie_select_quality_profile_scroll, EditMovieHandler, - movie_quality_profile_list, + quality_profile_list, ActiveRadarrBlock::EditMovieSelectQualityProfile, None ); @@ -451,6 +446,7 @@ mod tests { app.data.radarr_data.prompt_confirm_action, Some(RadarrEvent::EditMovie) ); + assert!(app.should_refresh); } #[test] @@ -567,7 +563,7 @@ mod tests { use rstest::rstest; use crate::app::radarr::radarr_test_utils::create_test_radarr_data; - use crate::{assert_edit_movie_reset, assert_movie_preferences_selections_reset}; + use crate::{assert_edit_media_reset, assert_preferences_selections_reset}; use super::*; @@ -611,13 +607,11 @@ mod tests { .handle(); assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - { - let radarr_data = &app.data.radarr_data; + let radarr_data = &app.data.radarr_data; - assert_movie_preferences_selections_reset!(radarr_data); - assert_edit_movie_reset!(radarr_data); - assert!(!radarr_data.prompt_confirm); - } + assert_preferences_selections_reset!(radarr_data); + assert_edit_media_reset!(radarr_data); + assert!(!radarr_data.prompt_confirm); } #[rstest] diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 4650b14..1b005d5 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -1,10 +1,11 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::{ - ActiveRadarrBlock, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, EDIT_MOVIE_BLOCKS, FILTER_BLOCKS, - MOVIE_DETAILS_BLOCKS, SEARCH_BLOCKS, + ActiveRadarrBlock, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, EDIT_COLLECTION_BLOCKS, + EDIT_MOVIE_BLOCKS, FILTER_BLOCKS, MOVIE_DETAILS_BLOCKS, SEARCH_BLOCKS, }; use crate::handlers::radarr_handlers::add_movie_handler::AddMovieHandler; use crate::handlers::radarr_handlers::collection_details_handler::CollectionDetailsHandler; +use crate::handlers::radarr_handlers::edit_collection_handler::EditCollectionHandler; use crate::handlers::radarr_handlers::edit_movie_handler::EditMovieHandler; use crate::handlers::radarr_handlers::movie_details_handler::MovieDetailsHandler; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; @@ -15,6 +16,7 @@ use crate::{handle_text_box_keys, handle_text_box_left_right_keys, App, Key}; mod add_movie_handler; mod collection_details_handler; +mod edit_collection_handler; mod edit_movie_handler; mod movie_details_handler; @@ -42,6 +44,10 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { _ if EDIT_MOVIE_BLOCKS.contains(self.active_radarr_block) => { EditMovieHandler::with(self.key, self.app, self.active_radarr_block, self.context).handle() } + _ if EDIT_COLLECTION_BLOCKS.contains(self.active_radarr_block) => { + EditCollectionHandler::with(self.key, self.app, self.active_radarr_block, self.context) + .handle() + } _ => self.handle_key_event(), } } @@ -447,9 +453,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { .push_navigation_stack(ActiveRadarrBlock::UpdateAllMoviesPrompt.into()); } _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { - self - .app - .pop_and_push_navigation_stack((*self.active_radarr_block).into()); + self.app.should_refresh = true; } _ => (), }, @@ -460,9 +464,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { .push_navigation_stack(ActiveRadarrBlock::UpdateDownloadsPrompt.into()); } _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { - self - .app - .pop_and_push_navigation_stack((*self.active_radarr_block).into()); + self.app.should_refresh = true; } _ => (), }, @@ -481,15 +483,25 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { self.app.data.radarr_data.is_filtering = true; self.app.should_ignore_quit_key = true; } + _ if *key == DEFAULT_KEYBINDINGS.edit.key => { + self.app.push_navigation_stack( + ( + ActiveRadarrBlock::EditCollectionPrompt, + Some(ActiveRadarrBlock::Collections), + ) + .into(), + ); + self.app.data.radarr_data.populate_edit_collection_fields(); + self.app.data.radarr_data.selected_block = + ActiveRadarrBlock::EditCollectionToggleMonitored; + } _ if *key == DEFAULT_KEYBINDINGS.update.key => { self .app .push_navigation_stack(ActiveRadarrBlock::UpdateAllCollectionsPrompt.into()); } _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { - self - .app - .pop_and_push_navigation_stack((*self.active_radarr_block).into()); + self.app.should_refresh = true; } _ => (), }, @@ -558,6 +570,7 @@ mod radarr_handler_test_utils { edit_path: HorizontallyScrollableText::default(), edit_tags: HorizontallyScrollableText::default(), edit_monitored: None, + edit_search_on_add: None, quality_profile_map: BiMap::from_iter([ (2222, "HD - 1080p".to_owned()), (1111, "Any".to_owned()), @@ -587,26 +600,26 @@ mod radarr_handler_test_utils { ActiveRadarrBlock::EditMovieToggleMonitored ); assert_eq!( - app.data.radarr_data.movie_minimum_availability_list.items, + app.data.radarr_data.minimum_availability_list.items, Vec::from_iter(MinimumAvailability::iter()) ); assert_eq!( app .data .radarr_data - .movie_minimum_availability_list + .minimum_availability_list .current_selection(), &MinimumAvailability::Released ); assert_eq!( - app.data.radarr_data.movie_quality_profile_list.items, + app.data.radarr_data.quality_profile_list.items, vec!["Any".to_owned(), "HD - 1080p".to_owned()] ); assert_str_eq!( app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .current_selection(), "HD - 1080p" ); @@ -615,6 +628,72 @@ mod radarr_handler_test_utils { assert_eq!(app.data.radarr_data.edit_monitored, Some(true)); }; } + + #[macro_export] + macro_rules! test_edit_collection_key { + ($handler:ident, $block:expr, $context:expr) => { + let mut app = App::default(); + let mut radarr_data = RadarrData { + edit_path: HorizontallyScrollableText::default(), + edit_tags: HorizontallyScrollableText::default(), + edit_monitored: None, + edit_search_on_add: None, + quality_profile_map: BiMap::from_iter([ + (2222, "HD - 1080p".to_owned()), + (1111, "Any".to_owned()), + ]), + filtered_collections: StatefulTable::default(), + ..create_test_radarr_data() + }; + radarr_data.collections.set_items(vec![Collection { + root_folder_path: "/nfs/movies/Test".to_owned().into(), + monitored: true, + search_on_add: true, + quality_profile_id: Number::from(2222), + minimum_availability: MinimumAvailability::Released, + ..Collection::default() + }]); + app.data.radarr_data = radarr_data; + + $handler::with(&DEFAULT_KEYBINDINGS.edit.key, &mut app, &$block, &None).handle(); + + assert_eq!( + app.get_current_route(), + &(ActiveRadarrBlock::EditCollectionPrompt, Some($context)).into() + ); + assert_eq!( + app.data.radarr_data.selected_block, + ActiveRadarrBlock::EditCollectionToggleMonitored + ); + assert_eq!( + app.data.radarr_data.minimum_availability_list.items, + Vec::from_iter(MinimumAvailability::iter()) + ); + assert_eq!( + app + .data + .radarr_data + .minimum_availability_list + .current_selection(), + &MinimumAvailability::Released + ); + assert_eq!( + app.data.radarr_data.quality_profile_list.items, + vec!["Any".to_owned(), "HD - 1080p".to_owned()] + ); + assert_str_eq!( + app + .data + .radarr_data + .quality_profile_list + .current_selection(), + "HD - 1080p" + ); + assert_str_eq!(app.data.radarr_data.edit_path.text, "/nfs/movies/Test"); + assert_eq!(app.data.radarr_data.edit_monitored, Some(true)); + assert_eq!(app.data.radarr_data.edit_search_on_add, Some(true)); + }; + } } #[cfg(test)] @@ -1382,6 +1461,15 @@ mod tests { ); } + #[test] + fn test_collection_edit_key() { + test_edit_collection_key!( + RadarrHandler, + ActiveRadarrBlock::Collections, + ActiveRadarrBlock::Collections + ); + } + #[rstest] #[case(ActiveRadarrBlock::Movies, ActiveRadarrBlock::UpdateAllMoviesPrompt)] #[case(ActiveRadarrBlock::Downloads, ActiveRadarrBlock::UpdateDownloadsPrompt)] @@ -1416,6 +1504,7 @@ mod tests { active_radarr_block: ActiveRadarrBlock, ) { let mut app = App::default(); + app.push_navigation_stack(active_radarr_block.into()); RadarrHandler::with( &DEFAULT_KEYBINDINGS.refresh.key, @@ -1426,7 +1515,7 @@ mod tests { .handle(); assert_eq!(app.get_current_route(), &active_radarr_block.into()); - assert!(app.is_routing); + assert!(app.should_refresh); } #[rstest] @@ -1691,4 +1780,19 @@ mod tests { ) { test_handler_delegation!(ActiveRadarrBlock::Movies, active_radarr_block); } + + #[rstest] + fn test_delegate_edit_collection_blocks_to_edit_collection_handler( + #[values( + ActiveRadarrBlock::EditCollectionPrompt, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + ActiveRadarrBlock::EditCollectionSelectQualityProfile, + ActiveRadarrBlock::EditCollectionToggleSearchOnAdd, + ActiveRadarrBlock::EditCollectionToggleMonitored + )] + active_radarr_block: ActiveRadarrBlock, + ) { + test_handler_delegation!(ActiveRadarrBlock::Collections, active_radarr_block); + } } diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 29717cf..745ff2c 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -83,11 +83,15 @@ pub struct CollectionMovie { #[derivative(Default)] #[serde(rename_all = "camelCase")] pub struct Collection { + #[derivative(Default(value = "Number::from(0)"))] + pub id: Number, #[serde(default)] pub title: HorizontallyScrollableText, pub root_folder_path: Option, pub search_on_add: bool, + pub monitored: bool, pub overview: Option, + pub minimum_availability: MinimumAvailability, #[derivative(Default(value = "Number::from(0)"))] pub quality_profile_id: Number, pub movies: Option>, diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index d0e747c..4e1f064 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -24,6 +24,7 @@ pub enum RadarrEvent { DeleteMovie, DownloadRelease, EditMovie, + EditCollection, GetCollections, GetDownloads, GetMovieCredits, @@ -48,7 +49,7 @@ pub enum RadarrEvent { impl RadarrEvent { const fn resource(self) -> &'static str { match self { - RadarrEvent::GetCollections => "/collection", + RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection", RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue", RadarrEvent::AddMovie | RadarrEvent::EditMovie @@ -88,6 +89,7 @@ impl<'a> Network<'a> { RadarrEvent::DeleteDownload => self.delete_download().await, RadarrEvent::DownloadRelease => self.download_release().await, RadarrEvent::EditMovie => self.edit_movie().await, + RadarrEvent::EditCollection => self.edit_collection().await, RadarrEvent::GetCollections => self.get_collections().await, RadarrEvent::GetDownloads => self.get_downloads().await, RadarrEvent::GetMovieCredits => self.get_credits().await, @@ -762,13 +764,13 @@ impl<'a> Network<'a> { let monitor = app .data .radarr_data - .movie_monitor_list + .monitor_list .current_selection() .to_string(); let minimum_availability = app .data .radarr_data - .movie_minimum_availability_list + .minimum_availability_list .current_selection() .to_string(); @@ -827,8 +829,8 @@ impl<'a> Network<'a> { let quality_profile_id = self.extract_quality_profile_id().await; let tag_ids_vec = self.extract_and_add_tag_ids_vec().await; let mut app = self.app.lock().await; - let mut detailed_movie_body: Value = serde_json::from_str(&app.response).unwrap(); - app.response = String::default(); + let response = app.response.drain(..).collect::(); + let mut detailed_movie_body: Value = serde_json::from_str(&response).unwrap(); let path: String = app.data.radarr_data.edit_path.drain(); @@ -836,7 +838,7 @@ impl<'a> Network<'a> { let minimum_availability = app .data .radarr_data - .movie_minimum_availability_list + .minimum_availability_list .current_selection() .to_string(); @@ -864,6 +866,82 @@ impl<'a> Network<'a> { .await; } + async fn edit_collection(&self) { + info!("Editing Radarr collection"); + + info!("Fetching collection details"); + let collection_id = self.extract_collection_id().await; + let request_props = self + .radarr_request_props_from( + format!( + "{}/{}", + RadarrEvent::GetCollections.resource(), + collection_id + ) + .as_str(), + RequestMethod::Get, + None::<()>, + ) + .await; + + self + .handle_request::<(), Value>(request_props, |detailed_collection_body, mut app| { + app.response = detailed_collection_body.to_string() + }) + .await; + + info!("Constructing edit collection body"); + + let body = { + let quality_profile_id = self.extract_quality_profile_id().await; + let mut app = self.app.lock().await; + let response = app.response.drain(..).collect::(); + let mut detailed_collection_body: Value = serde_json::from_str(&response).unwrap(); + + let root_folder_path: String = app.data.radarr_data.edit_path.drain(); + + let monitored = app.data.radarr_data.edit_monitored.unwrap_or_default(); + let search_on_add = app.data.radarr_data.edit_search_on_add.unwrap_or_default(); + let minimum_availability = app + .data + .radarr_data + .minimum_availability_list + .current_selection() + .to_string(); + + *detailed_collection_body.get_mut("monitored").unwrap() = json!(monitored); + *detailed_collection_body + .get_mut("minimumAvailability") + .unwrap() = json!(minimum_availability); + *detailed_collection_body + .get_mut("qualityProfileId") + .unwrap() = json!(quality_profile_id); + *detailed_collection_body.get_mut("rootFolderPath").unwrap() = json!(root_folder_path); + *detailed_collection_body.get_mut("searchOnAdd").unwrap() = json!(search_on_add); + + detailed_collection_body + }; + + debug!("Edit collection body: {:?}", body); + + let request_props = self + .radarr_request_props_from( + format!( + "{}/{}", + RadarrEvent::EditCollection.resource(), + collection_id + ) + .as_str(), + RequestMethod::Put, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + async fn download_release(&self) { let (guid, title, indexer_id) = { let app = self.app.lock().await; @@ -899,7 +977,7 @@ impl<'a> Network<'a> { let quality_profile = app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .current_selection(); *app .data @@ -988,6 +1066,43 @@ impl<'a> Network<'a> { } } + async fn extract_collection_id(&self) -> u64 { + if !self + .app + .lock() + .await + .data + .radarr_data + .filtered_collections + .items + .is_empty() + { + self + .app + .lock() + .await + .data + .radarr_data + .filtered_collections + .current_selection() + .id + .as_u64() + .unwrap() + } else { + self + .app + .lock() + .await + .data + .radarr_data + .collections + .current_selection() + .id + .as_u64() + .unwrap() + } + } + async fn append_movie_id_param(&self, resource: &str) -> String { let movie_id = self.extract_movie_id().await; format!("{}?movieId={}", resource, movie_id) @@ -1117,8 +1232,12 @@ mod test { } }, "collection": { + "id": 123, "title": "Test Collection", + "rootFolderPath": "/nfs/movies", "searchOnAdd": true, + "monitored": true, + "minimumAvailability": "released", "overview": "Collection blah blah blah", "qualityProfileId": 2222, "movies": [ @@ -1791,8 +1910,12 @@ mod test { #[tokio::test] async fn test_handle_get_collections_event() { let collection_json = json!([{ + "id": 123, "title": "Test Collection", + "rootFolderPath": "/nfs/movies", "searchOnAdd": true, + "monitored": true, + "minimumAvailability": "released", "overview": "Collection blah blah blah", "qualityProfileId": 2222, "movies": [{ @@ -2110,17 +2233,17 @@ mod test { app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .set_items(vec!["HD - 1080p".to_owned()]); app .data .radarr_data - .movie_monitor_list + .monitor_list .set_items(Vec::from_iter(Monitor::iter())); app .data .radarr_data - .movie_minimum_availability_list + .minimum_availability_list .set_items(Vec::from_iter(MinimumAvailability::iter())); if collection_details_context { app @@ -2180,12 +2303,12 @@ mod test { app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); app .data .radarr_data - .movie_minimum_availability_list + .minimum_availability_list .set_items(Vec::from_iter(MinimumAvailability::iter())); app.data.radarr_data.movies.set_items(vec![Movie { monitored: false, @@ -2201,11 +2324,105 @@ mod test { async_details_server.assert_async().await; async_edit_server.assert_async().await; + let app = app_arc.lock().await; + assert!(app.response.is_empty()); + assert!(app.data.radarr_data.edit_path.text.is_empty()); + assert!(app.data.radarr_data.movie_details.items.is_empty()); + } + + #[tokio::test] + async fn test_handle_edit_collection_event() { + let detailed_collection_body = json!({ + "id": 123, + "title": "Test 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_body = detailed_collection_body.clone(); + *expected_body.get_mut("monitored").unwrap() = json!(false); + *expected_body.get_mut("minimumAvailability").unwrap() = json!("announced"); + *expected_body.get_mut("qualityProfileId").unwrap() = json!(1111); + *expected_body.get_mut("rootFolderPath").unwrap() = json!("/nfs/Test Path"); + *expected_body.get_mut("searchOnAdd").unwrap() = json!(false); + + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(detailed_collection_body), + format!("{}/123", RadarrEvent::GetCollections.resource()).as_str(), + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/123", RadarrEvent::EditCollection.resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; { - let app = app_arc.lock().await; - assert!(app.data.radarr_data.edit_path.text.is_empty()); - assert!(app.data.radarr_data.movie_details.items.is_empty()); + let mut app = app_arc.lock().await; + app.data.radarr_data.edit_path = "/nfs/Test Path".to_owned().into(); + app.data.radarr_data.edit_monitored = Some(false); + app.data.radarr_data.edit_search_on_add = Some(false); + app + .data + .radarr_data + .quality_profile_list + .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); + app + .data + .radarr_data + .minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + app.data.radarr_data.collections.set_items(vec![Collection { + monitored: false, + search_on_add: false, + ..collection() + }]); + app.data.radarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); } + let network = Network::new(reqwest::Client::new(), &app_arc); + + network + .handle_radarr_event(RadarrEvent::EditCollection) + .await; + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + + let app = app_arc.lock().await; + assert!(app.response.is_empty()); + assert!(app.data.radarr_data.edit_path.text.is_empty()); + assert!(app.data.radarr_data.movie_details.items.is_empty()); } #[tokio::test] @@ -2244,7 +2461,7 @@ mod test { app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); app.data.radarr_data.quality_profile_map = BiMap::from_iter([(1, "Any".to_owned()), (2, "HD - 1080p".to_owned())]); @@ -2338,6 +2555,42 @@ mod test { assert_eq!(network.extract_movie_id().await, 1); } + #[tokio::test] + async fn test_extract_collection_id() { + let app_arc = Arc::new(Mutex::new(App::default())); + app_arc + .lock() + .await + .data + .radarr_data + .collections + .set_items(vec![Collection { + id: Number::from(1), + ..Collection::default() + }]); + let network = Network::new(reqwest::Client::new(), &app_arc); + + assert_eq!(network.extract_collection_id().await, 1); + } + + #[tokio::test] + async fn test_extract_collection_id_filtered_collection() { + let app_arc = Arc::new(Mutex::new(App::default())); + app_arc + .lock() + .await + .data + .radarr_data + .filtered_collections + .set_items(vec![Collection { + id: Number::from(1), + ..Collection::default() + }]); + let network = Network::new(reqwest::Client::new(), &app_arc); + + assert_eq!(network.extract_collection_id().await, 1); + } + #[tokio::test] async fn test_append_movie_id_param() { let app_arc = Arc::new(Mutex::new(App::default())); @@ -2562,9 +2815,12 @@ mod test { fn collection() -> Collection { Collection { + id: Number::from(123), title: "Test Collection".to_owned().into(), - root_folder_path: None, + root_folder_path: Some("/nfs/movies".to_owned()), search_on_add: true, + monitored: true, + minimum_availability: MinimumAvailability::Released, overview: Some("Collection blah blah blah".to_owned()), quality_profile_id: Number::from(2222), movies: Some(vec![collection_movie()]), diff --git a/src/ui/radarr_ui/add_movie_ui.rs b/src/ui/radarr_ui/add_movie_ui.rs index 0e5d010..0d8272a 100644 --- a/src/ui/radarr_ui/add_movie_ui.rs +++ b/src/ui/radarr_ui/add_movie_ui.rs @@ -272,7 +272,7 @@ fn draw_select_monitor_popup(f: &mut Frame<'_, B>, app: &mut App, po draw_drop_down_list( f, popup_area, - &mut app.data.radarr_data.movie_monitor_list, + &mut app.data.radarr_data.monitor_list, |monitor| ListItem::new(monitor.to_display_str().to_owned()), ); } @@ -319,16 +319,16 @@ fn draw_confirmation_prompt(f: &mut Frame<'_, B>, app: &mut App, pro let selected_block = &app.data.radarr_data.selected_block; let highlight_yes_no = *selected_block == ActiveRadarrBlock::AddMovieConfirmPrompt; - let selected_monitor = app.data.radarr_data.movie_monitor_list.current_selection(); + let selected_monitor = app.data.radarr_data.monitor_list.current_selection(); let selected_minimum_availability = app .data .radarr_data - .movie_minimum_availability_list + .minimum_availability_list .current_selection(); let selected_quality_profile = app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .current_selection(); f.render_widget(title_block_centered(title), prompt_area); diff --git a/src/ui/radarr_ui/collection_details_ui.rs b/src/ui/radarr_ui/collection_details_ui.rs index 5bab8a5..82bb0bd 100644 --- a/src/ui/radarr_ui/collection_details_ui.rs +++ b/src/ui/radarr_ui/collection_details_ui.rs @@ -21,8 +21,8 @@ pub(super) fn draw_collection_details_popup( app: &mut App, content_area: Rect, ) { - if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { - match active_radarr_block { + if let Route::Radarr(active_radarr_block, context_option) = app.get_current_route() { + match context_option.as_ref().unwrap_or(active_radarr_block) { ActiveRadarrBlock::ViewMovieOverview => { draw_small_popup_over( f, @@ -45,8 +45,8 @@ pub(super) fn draw_collection_details( ) { let chunks = vertical_chunks_with_margin( vec![ - Constraint::Percentage(20), - Constraint::Percentage(75), + Constraint::Percentage(25), + Constraint::Percentage(70), Constraint::Percentage(5), ], content_area, @@ -81,6 +81,17 @@ pub(super) fn draw_collection_details( let mut help_text = Text::from("<↑↓> scroll table | show overview/add movie | close"); help_text.patch_style(style_help()); + let monitored = if collection_selection.monitored { + "Yes" + } else { + "No" + }; + let search_on_add = if collection_selection.search_on_add { + "Yes" + } else { + "No" + }; + let minimum_availability = collection_selection.minimum_availability.to_display_str(); let collection_description = Text::from(vec![ spans_info_primary( @@ -94,11 +105,13 @@ pub(super) fn draw_collection_details( .clone() .unwrap_or_default(), ), - spans_info_primary( - "Search on Add: ".to_owned(), - collection_selection.search_on_add.to_string(), - ), spans_info_primary("Quality Profile: ".to_owned(), quality_profile), + spans_info_primary( + "Minimum Availability: ".to_owned(), + minimum_availability.to_owned(), + ), + spans_info_primary("Monitored: ".to_owned(), monitored.to_owned()), + spans_info_primary("Search on Add: ".to_owned(), search_on_add.to_owned()), ]); let description_paragraph = Paragraph::new(collection_description) diff --git a/src/ui/radarr_ui/edit_collection_ui.rs b/src/ui/radarr_ui/edit_collection_ui.rs new file mode 100644 index 0000000..d93e4c1 --- /dev/null +++ b/src/ui/radarr_ui/edit_collection_ui.rs @@ -0,0 +1,194 @@ +use tui::backend::Backend; +use tui::layout::{Constraint, Rect}; +use tui::Frame; + +use crate::app::radarr::ActiveRadarrBlock; +use crate::app::App; +use crate::models::Route; +use crate::ui::radarr_ui::{ + draw_select_minimum_availability_popup, draw_select_quality_profile_popup, +}; +use crate::ui::utils::{ + horizontal_chunks, layout_paragraph_borderless, title_block_centered, vertical_chunks_with_margin, +}; +use crate::ui::{ + draw_button, draw_checkbox_with_label, draw_drop_down_menu_button, draw_drop_down_popup, + draw_text_box_with_label, +}; + +pub(super) fn draw_edit_collection_prompt( + f: &mut Frame<'_, B>, + app: &mut App, + prompt_area: Rect, +) { + if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + match active_radarr_block { + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability => { + draw_drop_down_popup( + f, + app, + prompt_area, + draw_edit_collection_confirmation_prompt, + draw_select_minimum_availability_popup, + ); + } + ActiveRadarrBlock::EditCollectionSelectQualityProfile => { + draw_drop_down_popup( + f, + app, + prompt_area, + draw_edit_collection_confirmation_prompt, + draw_select_quality_profile_popup, + ); + } + ActiveRadarrBlock::EditCollectionPrompt + | ActiveRadarrBlock::EditCollectionToggleMonitored + | ActiveRadarrBlock::EditCollectionRootFolderPathInput + | ActiveRadarrBlock::EditCollectionToggleSearchOnAdd => { + draw_edit_collection_confirmation_prompt(f, app, prompt_area) + } + _ => (), + } + } +} + +fn draw_edit_collection_confirmation_prompt( + f: &mut Frame<'_, B>, + app: &mut App, + prompt_area: Rect, +) { + let (collection_title, collection_overview) = + if app.data.radarr_data.filtered_collections.items.is_empty() { + ( + app + .data + .radarr_data + .collections + .current_selection() + .title + .to_string(), + app + .data + .radarr_data + .collections + .current_selection() + .overview + .clone() + .unwrap_or_default(), + ) + } else { + ( + app + .data + .radarr_data + .filtered_collections + .current_selection() + .title + .to_string(), + app + .data + .radarr_data + .filtered_collections + .current_selection() + .overview + .clone() + .unwrap_or_default(), + ) + }; + let title = format!("Edit - {}", collection_title); + let yes_no_value = &app.data.radarr_data.prompt_confirm; + let selected_block = &app.data.radarr_data.selected_block; + let highlight_yes_no = *selected_block == ActiveRadarrBlock::EditCollectionConfirmPrompt; + + let selected_minimum_availability = app + .data + .radarr_data + .minimum_availability_list + .current_selection(); + let selected_quality_profile = app + .data + .radarr_data + .quality_profile_list + .current_selection(); + + f.render_widget(title_block_centered(&title), prompt_area); + + let chunks = vertical_chunks_with_margin( + vec![ + Constraint::Percentage(35), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(3), + ], + prompt_area, + 1, + ); + + let prompt_paragraph = layout_paragraph_borderless(&collection_overview); + f.render_widget(prompt_paragraph, chunks[0]); + + let horizontal_chunks = horizontal_chunks( + vec![Constraint::Percentage(50), Constraint::Percentage(50)], + chunks[7], + ); + + draw_checkbox_with_label( + f, + chunks[1], + "Monitored", + app.data.radarr_data.edit_monitored.unwrap_or_default(), + *selected_block == ActiveRadarrBlock::EditCollectionToggleMonitored, + ); + + draw_drop_down_menu_button( + f, + chunks[2], + "Minimum Availability", + selected_minimum_availability.to_display_str(), + *selected_block == ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + ); + draw_drop_down_menu_button( + f, + chunks[3], + "Quality Profile", + selected_quality_profile, + *selected_block == ActiveRadarrBlock::EditCollectionSelectQualityProfile, + ); + + if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + draw_text_box_with_label( + f, + chunks[4], + "Root Folder", + &app.data.radarr_data.edit_path.text, + *app.data.radarr_data.edit_path.offset.borrow(), + *selected_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput, + active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput, + ); + } + + draw_checkbox_with_label( + f, + chunks[5], + "Search on Add", + app.data.radarr_data.edit_search_on_add.unwrap_or_default(), + *selected_block == ActiveRadarrBlock::EditCollectionToggleSearchOnAdd, + ); + + draw_button( + f, + horizontal_chunks[0], + "Save", + *yes_no_value && highlight_yes_no, + ); + draw_button( + f, + horizontal_chunks[1], + "Cancel", + !*yes_no_value && highlight_yes_no, + ); +} diff --git a/src/ui/radarr_ui/edit_movie_ui.rs b/src/ui/radarr_ui/edit_movie_ui.rs index 93bb269..fd80c1d 100644 --- a/src/ui/radarr_ui/edit_movie_ui.rs +++ b/src/ui/radarr_ui/edit_movie_ui.rs @@ -28,7 +28,7 @@ pub(super) fn draw_edit_movie_prompt( f, app, prompt_area, - draw_edit_confirmation_prompt, + draw_edit_movie_confirmation_prompt, draw_select_minimum_availability_popup, ); } @@ -37,40 +37,61 @@ pub(super) fn draw_edit_movie_prompt( f, app, prompt_area, - draw_edit_confirmation_prompt, + draw_edit_movie_confirmation_prompt, draw_select_quality_profile_popup, ); } ActiveRadarrBlock::EditMoviePrompt | ActiveRadarrBlock::EditMovieToggleMonitored | ActiveRadarrBlock::EditMoviePathInput - | ActiveRadarrBlock::EditMovieTagsInput => draw_edit_confirmation_prompt(f, app, prompt_area), + | ActiveRadarrBlock::EditMovieTagsInput => { + draw_edit_movie_confirmation_prompt(f, app, prompt_area) + } _ => (), } } } -fn draw_edit_confirmation_prompt( +fn draw_edit_movie_confirmation_prompt( f: &mut Frame<'_, B>, app: &mut App, prompt_area: Rect, ) { - let (movie_title, movie_overview) = ( - app - .data - .radarr_data - .movies - .current_selection() - .title - .to_string(), - app - .data - .radarr_data - .movies - .current_selection() - .overview - .clone(), - ); + let (movie_title, movie_overview) = if app.data.radarr_data.filtered_movies.items.is_empty() { + ( + app + .data + .radarr_data + .movies + .current_selection() + .title + .to_string(), + app + .data + .radarr_data + .movies + .current_selection() + .overview + .clone(), + ) + } else { + ( + app + .data + .radarr_data + .filtered_movies + .current_selection() + .title + .to_string(), + app + .data + .radarr_data + .filtered_movies + .current_selection() + .overview + .clone(), + ) + }; let title = format!("Edit - {}", movie_title); let yes_no_value = &app.data.radarr_data.prompt_confirm; let selected_block = &app.data.radarr_data.selected_block; @@ -79,12 +100,12 @@ fn draw_edit_confirmation_prompt( let selected_minimum_availability = app .data .radarr_data - .movie_minimum_availability_list + .minimum_availability_list .current_selection(); let selected_quality_profile = app .data .radarr_data - .movie_quality_profile_list + .quality_profile_list .current_selection(); f.render_widget(title_block_centered(&title), prompt_area); diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index ecfc76d..5ece98e 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -10,8 +10,8 @@ use tui::widgets::{Cell, ListItem, Paragraph, Row}; use tui::Frame; use crate::app::radarr::{ - ActiveRadarrBlock, RadarrData, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, EDIT_MOVIE_BLOCKS, - FILTER_BLOCKS, MOVIE_DETAILS_BLOCKS, SEARCH_BLOCKS, + ActiveRadarrBlock, RadarrData, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, + EDIT_COLLECTION_BLOCKS, EDIT_MOVIE_BLOCKS, FILTER_BLOCKS, MOVIE_DETAILS_BLOCKS, SEARCH_BLOCKS, }; use crate::app::App; use crate::logos::RADARR_LOGO; @@ -19,6 +19,7 @@ use crate::models::radarr_models::{Collection, DiskSpace, DownloadRecord, Movie} use crate::models::{HorizontallyScrollableText, Route}; use crate::ui::radarr_ui::add_movie_ui::draw_add_movie_search_popup; use crate::ui::radarr_ui::collection_details_ui::draw_collection_details_popup; +use crate::ui::radarr_ui::edit_collection_ui::draw_edit_collection_prompt; use crate::ui::radarr_ui::edit_movie_ui::draw_edit_movie_prompt; use crate::ui::radarr_ui::movie_details_ui::draw_movie_info_popup; use crate::ui::utils::{ @@ -35,6 +36,7 @@ use crate::utils::{convert_runtime, convert_to_gb}; mod add_movie_ui; mod collection_details_ui; +mod edit_collection_ui; mod edit_movie_ui; mod movie_details_ui; @@ -113,6 +115,30 @@ pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, ar } } } + _ if EDIT_COLLECTION_BLOCKS.contains(&active_radarr_block) => { + if let Some(context) = context_option { + match context { + ActiveRadarrBlock::Collections => draw_medium_popup_over( + f, + app, + content_rect, + draw_collections, + draw_edit_collection_prompt, + ), + _ if COLLECTION_DETAILS_BLOCKS.contains(&context) => { + draw_large_popup_over( + f, + app, + content_rect, + draw_collections, + draw_collection_details_popup, + ); + draw_popup(f, app, draw_edit_collection_prompt, 60, 60); + } + _ => (), + } + } + } ActiveRadarrBlock::DeleteMoviePrompt => { draw_prompt_popup_over(f, app, content_rect, draw_library, draw_delete_movie_prompt) } @@ -547,12 +573,20 @@ fn draw_collections(f: &mut Frame<'_, B>, app: &mut App, area: Rect) content, table_headers: vec![ "Collection", - "Search on Add?", "Number of Movies", "Root Folder Path", "Quality Profile", + "Search on Add", + "Monitored", + ], + constraints: vec![ + Constraint::Percentage(25), + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(15), ], - constraints: iter::repeat(Constraint::Ratio(1, 5)).take(5).collect(), help: app .data .radarr_data @@ -562,14 +596,19 @@ fn draw_collections(f: &mut Frame<'_, B>, app: &mut App, area: Rect) |collection| { let number_of_movies = collection.movies.clone().unwrap_or_default().len(); collection.title.scroll_left_or_reset( - get_width_from_percentage(area, 100 / 5), + 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(collection.search_on_add.to_string()), Cell::from(number_of_movies.to_string()), Cell::from(collection.root_folder_path.clone().unwrap_or_default()), Cell::from( @@ -578,6 +617,8 @@ fn draw_collections(f: &mut Frame<'_, B>, app: &mut App, area: Rect) .unwrap() .to_owned(), ), + Cell::from(search_on_add), + Cell::from(monitored), ]) .style(style_primary()) }, @@ -704,7 +745,7 @@ fn draw_select_minimum_availability_popup( draw_drop_down_list( f, popup_area, - &mut app.data.radarr_data.movie_minimum_availability_list, + &mut app.data.radarr_data.minimum_availability_list, |minimum_availability| ListItem::new(minimum_availability.to_display_str().to_owned()), ); } @@ -717,7 +758,7 @@ fn draw_select_quality_profile_popup( draw_drop_down_list( f, popup_area, - &mut app.data.radarr_data.movie_quality_profile_list, + &mut app.data.radarr_data.quality_profile_list, |quality_profile| ListItem::new(quality_profile.clone()), ); } diff --git a/src/ui/radarr_ui/movie_details_ui.rs b/src/ui/radarr_ui/movie_details_ui.rs index c3f32c2..38989d3 100644 --- a/src/ui/radarr_ui/movie_details_ui.rs +++ b/src/ui/radarr_ui/movie_details_ui.rs @@ -26,12 +26,7 @@ pub(super) fn draw_movie_info_popup(f: &mut Frame<'_, B>, app: &mut let (content_area, _) = draw_tabs(f, area, "Movie Info", &app.data.radarr_data.movie_info_tabs); if let Route::Radarr(active_radarr_block, context_option) = app.get_current_route() { - let match_block = if let Some(context) = context_option { - context - } else { - active_radarr_block - }; - match match_block { + match context_option.as_ref().unwrap_or(active_radarr_block) { ActiveRadarrBlock::AutomaticallySearchMoviePrompt => draw_prompt_popup_over( f, app, diff --git a/src/utils.rs b/src/utils.rs index e2b5ced..ffc74bb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -62,7 +62,7 @@ mod tests { } #[test] - fn test_strop_non_alphanumeric_characters() { + fn test_strip_non_alphanumeric_characters() { assert_eq!( strip_non_alphanumeric_characters("Te$t S7r!ng::'~-_}"), "tet s7rng".to_owned()