diff --git a/src/app/key_binding.rs b/src/app/key_binding.rs index 2236c36..860c576 100644 --- a/src/app/key_binding.rs +++ b/src/app/key_binding.rs @@ -18,6 +18,7 @@ generate_keybindings! { search, filter, sort, + edit, refresh, home, end, @@ -69,6 +70,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { key: Key::Char('o'), desc: "Sort", }, + edit: KeyBinding { + key: Key::Char('e'), + desc: "Edit", + }, refresh: KeyBinding { key: Key::Char('r'), desc: "Refresh", diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 5df0f56..faf0b61 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 add_movie_monitor_list: StatefulList, - pub add_movie_minimum_availability_list: StatefulList, - pub add_movie_quality_profile_list: StatefulList, + pub movie_monitor_list: StatefulList, + pub movie_minimum_availability_list: StatefulList, + pub movie_quality_profile_list: StatefulList, pub selected_block: ActiveRadarrBlock, pub downloads: StatefulTable, pub quality_profile_map: HashMap, @@ -43,6 +43,9 @@ pub struct RadarrData { pub movie_info_tabs: TabState, pub search: String, pub filter: String, + pub edit_path: String, + pub edit_tags: String, + pub edit_monitored: Option, pub sort_ascending: Option, pub prompt_confirm: bool, pub is_searching: bool, @@ -70,6 +73,13 @@ impl RadarrData { self.filtered_collections = StatefulTable::default(); } + pub fn reset_edit_movie(&mut self) { + self.edit_monitored = None; + self.edit_path = String::default(); + self.edit_tags = String::default(); + self.reset_movie_preferences_selections(); + } + pub fn reset_movie_info_tabs(&mut self) { self.file_details = String::default(); self.audio_details = String::default(); @@ -84,26 +94,68 @@ impl RadarrData { self.movie_info_tabs.index = 0; } - pub fn reset_add_movie_selections(&mut self) { - self.add_movie_monitor_list = StatefulList::default(); - self.add_movie_minimum_availability_list = StatefulList::default(); - self.add_movie_quality_profile_list = StatefulList::default(); + 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 populate_add_movie_preferences_lists(&mut self) { + pub fn populate_movie_preferences_lists(&mut self) { self - .add_movie_monitor_list + .movie_monitor_list .set_items(Vec::from_iter(Monitor::iter())); self - .add_movie_minimum_availability_list + .movie_minimum_availability_list .set_items(Vec::from_iter(MinimumAvailability::iter())); let mut quality_profile_names: Vec = self.quality_profile_map.values().cloned().collect(); quality_profile_names.sort(); self - .add_movie_quality_profile_list + .movie_quality_profile_list .set_items(quality_profile_names); } + + pub fn populate_edit_movie_fields(&mut self) { + self.populate_movie_preferences_lists(); + let Movie { + path, + monitored, + minimum_availability, + quality_profile_id, + .. + } = if self.filtered_movies.items.is_empty() { + self.movies.current_selection_clone() + } else { + self.filtered_movies.current_selection_clone() + }; + + self.edit_path = path; + self.edit_monitored = Some(monitored); + + let minimum_availability_index = self + .movie_minimum_availability_list + .items + .iter() + .position(|&ma| ma == minimum_availability); + self + .movie_minimum_availability_list + .state + .select(minimum_availability_index); + + let quality_profile_name = self + .quality_profile_map + .get(&quality_profile_id.as_u64().unwrap()) + .unwrap(); + let quality_profile_index = self + .movie_quality_profile_list + .items + .iter() + .position(|profile| profile == quality_profile_name); + self + .movie_quality_profile_list + .state + .select(quality_profile_index); + } } impl Default for RadarrData { @@ -115,9 +167,9 @@ impl Default for RadarrData { start_time: DateTime::default(), movies: StatefulTable::default(), add_searched_movies: StatefulTable::default(), - add_movie_monitor_list: StatefulList::default(), - add_movie_minimum_availability_list: StatefulList::default(), - add_movie_quality_profile_list: StatefulList::default(), + movie_monitor_list: StatefulList::default(), + movie_minimum_availability_list: StatefulList::default(), + movie_quality_profile_list: StatefulList::default(), selected_block: ActiveRadarrBlock::AddMovieSelectMonitor, filtered_movies: StatefulTable::default(), downloads: StatefulTable::default(), @@ -137,6 +189,9 @@ impl Default for RadarrData { prompt_confirm_action: None, search: String::default(), filter: String::default(), + edit_path: String::default(), + edit_tags: String::default(), + edit_monitored: None, sort_ascending: None, is_searching: false, is_filtering: false, @@ -146,7 +201,7 @@ impl Default for RadarrData { title: "Library".to_owned(), route: ActiveRadarrBlock::Movies.into(), help: String::default(), - contextual_help: Some(" add | search | filter | refresh | details | cancel filter | delete" + contextual_help: Some(" add | edit | search | filter | refresh | details | cancel filter | delete" .to_owned()), }, TabRoute { @@ -167,37 +222,37 @@ impl Default for RadarrData { TabRoute { title: "Details".to_owned(), route: ActiveRadarrBlock::MovieDetails.into(), - help: " refresh | auto search | close".to_owned(), + help: " refresh | edit | auto search | close".to_owned(), contextual_help: None }, TabRoute { title: "History".to_owned(), route: ActiveRadarrBlock::MovieHistory.into(), - help: " refresh | auto search | close".to_owned(), + help: " refresh | edit | auto search | close".to_owned(), contextual_help: None }, TabRoute { title: "File".to_owned(), route: ActiveRadarrBlock::FileInfo.into(), - help: " refresh | auto search | close".to_owned(), + help: " refresh | edit | auto search | close".to_owned(), contextual_help: None, }, TabRoute { title: "Cast".to_owned(), route: ActiveRadarrBlock::Cast.into(), - help: " refresh | auto search | close".to_owned(), + help: " refresh | edit | auto search | close".to_owned(), contextual_help: None, }, TabRoute { title: "Crew".to_owned(), route: ActiveRadarrBlock::Crew.into(), - help: " refresh | auto search | close".to_owned(), + help: " refresh | edit | auto search | close".to_owned(), contextual_help: None, }, TabRoute { title: "Manual Search".to_owned(), route: ActiveRadarrBlock::ManualSearch.into(), - help: " refresh | sort | auto search | close".to_owned(), + help: " refresh | edit | sort | auto search | close".to_owned(), contextual_help: Some(" details".to_owned()) } ]), @@ -223,6 +278,13 @@ pub enum ActiveRadarrBlock { DeleteMoviePrompt, DeleteDownloadPrompt, Downloads, + EditMoviePrompt, + EditMovieConfirmPrompt, + EditMoviePathInput, + EditMovieSelectMinimumAvailability, + EditMovieSelectQualityProfile, + EditMovieTagsInput, + EditMovieToggleMonitored, FileInfo, FilterCollections, FilterMovies, @@ -250,6 +312,15 @@ pub const ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 7] = [ ActiveRadarrBlock::AddMovieSelectQualityProfile, ActiveRadarrBlock::AddMovieAlreadyInLibrary, ]; +pub const EDIT_MOVIE_BLOCKS: [ActiveRadarrBlock; 7] = [ + ActiveRadarrBlock::EditMoviePrompt, + ActiveRadarrBlock::EditMovieConfirmPrompt, + ActiveRadarrBlock::EditMoviePathInput, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + ActiveRadarrBlock::EditMovieSelectQualityProfile, + ActiveRadarrBlock::EditMovieTagsInput, + ActiveRadarrBlock::EditMovieToggleMonitored, +]; pub const MOVIE_DETAILS_BLOCKS: [ActiveRadarrBlock; 10] = [ ActiveRadarrBlock::MovieDetails, ActiveRadarrBlock::MovieHistory, @@ -289,6 +360,21 @@ impl ActiveRadarrBlock { } } + pub fn next_edit_prompt_block(&self) -> Self { + match self { + ActiveRadarrBlock::EditMovieToggleMonitored => { + ActiveRadarrBlock::EditMovieSelectMinimumAvailability + } + ActiveRadarrBlock::EditMovieSelectMinimumAvailability => { + ActiveRadarrBlock::EditMovieSelectQualityProfile + } + ActiveRadarrBlock::EditMovieSelectQualityProfile => ActiveRadarrBlock::EditMoviePathInput, + ActiveRadarrBlock::EditMoviePathInput => ActiveRadarrBlock::EditMovieTagsInput, + ActiveRadarrBlock::EditMovieTagsInput => ActiveRadarrBlock::EditMovieConfirmPrompt, + _ => ActiveRadarrBlock::EditMovieToggleMonitored, + } + } + pub fn previous_add_prompt_block(&self) -> Self { match self { ActiveRadarrBlock::AddMovieSelectMonitor => ActiveRadarrBlock::AddMovieConfirmPrompt, @@ -302,6 +388,22 @@ impl ActiveRadarrBlock { _ => ActiveRadarrBlock::AddMovieSelectMonitor, } } + + pub fn previous_edit_prompt_block(&self) -> Self { + match self { + ActiveRadarrBlock::EditMovieToggleMonitored => ActiveRadarrBlock::EditMovieConfirmPrompt, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability => { + ActiveRadarrBlock::EditMovieToggleMonitored + } + ActiveRadarrBlock::EditMovieSelectQualityProfile => { + ActiveRadarrBlock::EditMovieSelectMinimumAvailability + } + ActiveRadarrBlock::EditMoviePathInput => ActiveRadarrBlock::EditMovieSelectQualityProfile, + ActiveRadarrBlock::EditMovieTagsInput => ActiveRadarrBlock::EditMoviePathInput, + ActiveRadarrBlock::EditMovieConfirmPrompt => ActiveRadarrBlock::EditMovieTagsInput, + _ => ActiveRadarrBlock::EditMovieToggleMonitored, + } + } } impl From for Route { @@ -330,6 +432,7 @@ impl App { self.is_loading = true; self.populate_movie_collection_table().await; self.is_loading = false; + self.check_for_prompt_action().await; } ActiveRadarrBlock::Downloads => { self.is_loading = true; @@ -481,6 +584,9 @@ pub mod radarr_test_utils { is_filtering: true, search: "test search".to_owned(), filter: "test filter".to_owned(), + edit_path: "test path".to_owned(), + edit_tags: "test tag".to_owned(), + edit_monitored: Some(true), file_details: "test file details".to_owned(), audio_details: "test audio details".to_owned(), video_details: "test video details".to_owned(), @@ -497,13 +603,13 @@ pub mod radarr_test_utils { .set_items(vec![Release::default()]); radarr_data.movie_info_tabs.index = 1; radarr_data - .add_movie_monitor_list + .movie_monitor_list .set_items(vec![Monitor::default()]); radarr_data - .add_movie_minimum_availability_list + .movie_minimum_availability_list .set_items(vec![MinimumAvailability::default()]); radarr_data - .add_movie_quality_profile_list + .movie_quality_profile_list .set_items(vec![String::default()]); radarr_data .movie_releases_sort @@ -544,6 +650,15 @@ pub mod radarr_test_utils { }; } + #[macro_export] + macro_rules! assert_edit_movie_reset { + ($radarr_data:expr) => { + assert!($radarr_data.edit_monitored.is_none()); + assert!($radarr_data.edit_path.is_empty()); + assert!($radarr_data.edit_tags.is_empty()); + }; + } + #[macro_export] macro_rules! assert_filter_reset { ($radarr_data:expr) => { @@ -572,14 +687,14 @@ pub mod radarr_test_utils { } #[macro_export] - macro_rules! assert_add_movie_selections_reset { + macro_rules! assert_movie_preferences_selections_reset { ($radarr_data:expr) => { - assert!($radarr_data.add_movie_monitor_list.items.is_empty()); + assert!($radarr_data.movie_monitor_list.items.is_empty()); assert!($radarr_data - .add_movie_minimum_availability_list + .movie_minimum_availability_list .items .is_empty()); - assert!($radarr_data.add_movie_quality_profile_list.items.is_empty()); + assert!($radarr_data.movie_quality_profile_list.items.is_empty()); }; } } @@ -590,12 +705,14 @@ mod tests { use std::collections::HashMap; use pretty_assertions::assert_eq; + use rstest::rstest; + use serde_json::Number; use strum::IntoEnumIterator; use crate::app::radarr::radarr_test_utils::create_test_radarr_data; use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; - use crate::models::radarr_models::{MinimumAvailability, Monitor}; - use crate::models::Route; + use crate::models::radarr_models::{MinimumAvailability, Monitor, Movie}; + use crate::models::{Route, StatefulTable}; #[test] fn test_from_tuple_to_route_with_context() { @@ -648,16 +765,30 @@ mod tests { } #[test] - fn test_reset_add_movie_selections() { - let mut radarr_data = create_test_radarr_data(); + fn test_reset_edit_movie() { + let mut radarr_data = RadarrData { + edit_monitored: Some(true), + edit_path: "test path".to_owned(), + edit_tags: "test tag".to_owned(), + ..RadarrData::default() + }; - radarr_data.reset_add_movie_selections(); + radarr_data.reset_edit_movie(); - assert_add_movie_selections_reset!(radarr_data); + assert_edit_movie_reset!(radarr_data); } #[test] - fn test_populate_add_movie_preferences_lists() { + fn test_reset_movie_preferences_selections() { + let mut radarr_data = create_test_radarr_data(); + + radarr_data.reset_movie_preferences_selections(); + + assert_movie_preferences_selections_reset!(radarr_data); + } + + #[test] + fn test_populate_movie_preferences_lists() { let mut radarr_data = RadarrData { quality_profile_map: HashMap::from([ (2222, "HD - 1080p".to_owned()), @@ -666,21 +797,72 @@ mod tests { ..RadarrData::default() }; - radarr_data.populate_add_movie_preferences_lists(); + radarr_data.populate_movie_preferences_lists(); assert_eq!( - radarr_data.add_movie_monitor_list.items, + radarr_data.movie_monitor_list.items, Vec::from_iter(Monitor::iter()) ); assert_eq!( - radarr_data.add_movie_minimum_availability_list.items, + radarr_data.movie_minimum_availability_list.items, Vec::from_iter(MinimumAvailability::iter()) ); assert_eq!( - radarr_data.add_movie_quality_profile_list.items, + radarr_data.movie_quality_profile_list.items, vec!["Any".to_owned(), "HD - 1080p".to_owned()] ); } + + #[rstest] + fn test_populate_edit_movie_fields(#[values(true, false)] test_filtered_movies: bool) { + let mut radarr_data = RadarrData { + edit_path: String::default(), + edit_tags: String::default(), + edit_monitored: None, + quality_profile_map: HashMap::from([ + (2222, "HD - 1080p".to_owned()), + (1111, "Any".to_owned()), + ]), + filtered_movies: StatefulTable::default(), + ..create_test_radarr_data() + }; + let movie = Movie { + path: "/nfs/movies/Test".to_owned(), + monitored: true, + quality_profile_id: Number::from(2222), + minimum_availability: MinimumAvailability::Released, + ..Movie::default() + }; + + if test_filtered_movies { + radarr_data.filtered_movies.set_items(vec![movie]); + } else { + radarr_data.movies.set_items(vec![movie]); + } + + radarr_data.populate_edit_movie_fields(); + + assert_eq!( + radarr_data.movie_minimum_availability_list.items, + Vec::from_iter(MinimumAvailability::iter()) + ); + assert_eq!( + radarr_data + .movie_minimum_availability_list + .current_selection(), + &MinimumAvailability::Released + ); + assert_eq!( + radarr_data.movie_quality_profile_list.items, + vec!["Any".to_owned(), "HD - 1080p".to_owned()] + ); + assert_eq!( + radarr_data.movie_quality_profile_list.current_selection(), + "HD - 1080p" + ); + assert_eq!(radarr_data.edit_path, "/nfs/movies/Test".to_owned()); + assert_eq!(radarr_data.edit_monitored, Some(true)); + } } mod active_radarr_block_tests { @@ -713,6 +895,39 @@ mod tests { assert_eq!(active_block, ActiveRadarrBlock::AddMovieSelectMonitor); } + #[test] + fn test_next_edit_prompt_block() { + let active_block = ActiveRadarrBlock::EditMovieToggleMonitored.next_edit_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability + ); + + let active_block = active_block.next_edit_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditMovieSelectQualityProfile + ); + + let active_block = active_block.next_edit_prompt_block(); + + assert_eq!(active_block, ActiveRadarrBlock::EditMoviePathInput); + + let active_block = active_block.next_edit_prompt_block(); + + assert_eq!(active_block, ActiveRadarrBlock::EditMovieTagsInput); + + let active_block = active_block.next_edit_prompt_block(); + + assert_eq!(active_block, ActiveRadarrBlock::EditMovieConfirmPrompt); + + let active_block = active_block.next_edit_prompt_block(); + + assert_eq!(active_block, ActiveRadarrBlock::EditMovieToggleMonitored); + } + #[test] fn test_previous_add_prompt_block() { let active_block = ActiveRadarrBlock::AddMovieSelectMonitor.previous_add_prompt_block(); @@ -737,6 +952,39 @@ mod tests { assert_eq!(active_block, ActiveRadarrBlock::AddMovieSelectMonitor); } + + #[test] + fn test_previous_edit_prompt_block() { + let active_block = ActiveRadarrBlock::EditMovieToggleMonitored.previous_edit_prompt_block(); + + assert_eq!(active_block, ActiveRadarrBlock::EditMovieConfirmPrompt); + + let active_block = active_block.previous_edit_prompt_block(); + + assert_eq!(active_block, ActiveRadarrBlock::EditMovieTagsInput); + + let active_block = active_block.previous_edit_prompt_block(); + + assert_eq!(active_block, ActiveRadarrBlock::EditMoviePathInput); + + let active_block = active_block.previous_edit_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditMovieSelectQualityProfile + ); + + let active_block = active_block.previous_edit_prompt_block(); + + assert_eq!( + active_block, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability + ); + + let active_block = active_block.previous_edit_prompt_block(); + + assert_eq!(active_block, ActiveRadarrBlock::EditMovieToggleMonitored); + } } mod radarr_tests { @@ -771,7 +1019,8 @@ mod tests { #[tokio::test] async fn test_dispatch_by_collection_details_block() { - let mut app = App::default(); + let (mut app, mut sync_network_rx) = construct_app_unit(); + app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::AddMovie); app.data.radarr_data.collections.set_items(vec![Collection { movies: Some(vec![CollectionMovie::default()]), @@ -783,8 +1032,13 @@ mod tests { .await; assert!(!app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::AddMovie.into() + ); assert!(!app.data.radarr_data.collection_movies.items.is_empty()); assert_eq!(app.tick_count, 0); + assert!(!app.data.radarr_data.prompt_confirm); } #[tokio::test] diff --git a/src/handlers/radarr_handlers/add_movie_handler.rs b/src/handlers/radarr_handlers/add_movie_handler.rs index d888bfc..d309425 100644 --- a/src/handlers/radarr_handlers/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/add_movie_handler.rs @@ -37,19 +37,19 @@ 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.add_movie_monitor_list.scroll_up() + self.app.data.radarr_data.movie_monitor_list.scroll_up() } ActiveRadarrBlock::AddMovieSelectMinimumAvailability => self .app .data .radarr_data - .add_movie_minimum_availability_list + .movie_minimum_availability_list .scroll_up(), ActiveRadarrBlock::AddMovieSelectQualityProfile => self .app .data .radarr_data - .add_movie_quality_profile_list + .movie_quality_profile_list .scroll_up(), ActiveRadarrBlock::AddMoviePrompt => { self.app.data.radarr_data.selected_block = self @@ -69,23 +69,20 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { ActiveRadarrBlock::AddMovieSearchResults => { self.app.data.radarr_data.add_searched_movies.scroll_down() } - ActiveRadarrBlock::AddMovieSelectMonitor => self - .app - .data - .radarr_data - .add_movie_monitor_list - .scroll_down(), + ActiveRadarrBlock::AddMovieSelectMonitor => { + self.app.data.radarr_data.movie_monitor_list.scroll_down() + } ActiveRadarrBlock::AddMovieSelectMinimumAvailability => self .app .data .radarr_data - .add_movie_minimum_availability_list + .movie_minimum_availability_list .scroll_down(), ActiveRadarrBlock::AddMovieSelectQualityProfile => self .app .data .radarr_data - .add_movie_quality_profile_list + .movie_quality_profile_list .scroll_down(), ActiveRadarrBlock::AddMoviePrompt => { self.app.data.radarr_data.selected_block = self @@ -107,23 +104,20 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { .radarr_data .add_searched_movies .scroll_to_top(), - ActiveRadarrBlock::AddMovieSelectMonitor => self - .app - .data - .radarr_data - .add_movie_monitor_list - .scroll_to_top(), + ActiveRadarrBlock::AddMovieSelectMonitor => { + self.app.data.radarr_data.movie_monitor_list.scroll_to_top() + } ActiveRadarrBlock::AddMovieSelectMinimumAvailability => self .app .data .radarr_data - .add_movie_minimum_availability_list + .movie_minimum_availability_list .scroll_to_top(), ActiveRadarrBlock::AddMovieSelectQualityProfile => self .app .data .radarr_data - .add_movie_quality_profile_list + .movie_quality_profile_list .scroll_to_top(), _ => (), } @@ -141,19 +135,19 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { .app .data .radarr_data - .add_movie_monitor_list + .movie_monitor_list .scroll_to_bottom(), ActiveRadarrBlock::AddMovieSelectMinimumAvailability => self .app .data .radarr_data - .add_movie_minimum_availability_list + .movie_minimum_availability_list .scroll_to_bottom(), ActiveRadarrBlock::AddMovieSelectQualityProfile => self .app .data .radarr_data - .add_movie_quality_profile_list + .movie_quality_profile_list .scroll_to_bottom(), _ => (), } @@ -209,11 +203,8 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { self .app .push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); - self - .app - .data - .radarr_data - .populate_add_movie_preferences_lists(); + self.app.data.radarr_data.populate_movie_preferences_lists(); + self.app.data.radarr_data.selected_block = ActiveRadarrBlock::AddMovieSelectMonitor } } ActiveRadarrBlock::AddMoviePrompt => match self.app.data.radarr_data.selected_block { @@ -225,23 +216,11 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { self.app.pop_navigation_stack(); } } - ActiveRadarrBlock::AddMovieSelectMonitor => self + ActiveRadarrBlock::AddMovieSelectMonitor + | ActiveRadarrBlock::AddMovieSelectMinimumAvailability + | ActiveRadarrBlock::AddMovieSelectQualityProfile => self .app - .push_navigation_stack((ActiveRadarrBlock::AddMovieSelectMonitor, *self.context).into()), - ActiveRadarrBlock::AddMovieSelectMinimumAvailability => self.app.push_navigation_stack( - ( - ActiveRadarrBlock::AddMovieSelectMinimumAvailability, - *self.context, - ) - .into(), - ), - ActiveRadarrBlock::AddMovieSelectQualityProfile => self.app.push_navigation_stack( - ( - ActiveRadarrBlock::AddMovieSelectQualityProfile, - *self.context, - ) - .into(), - ), + .push_navigation_stack((self.app.data.radarr_data.selected_block, *self.context).into()), _ => (), }, ActiveRadarrBlock::AddMovieSelectMonitor @@ -265,7 +244,11 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { } ActiveRadarrBlock::AddMoviePrompt => { self.app.pop_navigation_stack(); - self.app.data.radarr_data.reset_add_movie_selections(); + self + .app + .data + .radarr_data + .reset_movie_preferences_selections(); self.app.data.radarr_data.prompt_confirm = false; } ActiveRadarrBlock::AddMovieSelectMonitor @@ -322,7 +305,7 @@ mod tests { test_add_movie_select_monitor_scroll, AddMovieHandler, Monitor, - add_movie_monitor_list, + movie_monitor_list, ActiveRadarrBlock::AddMovieSelectMonitor, None ); @@ -331,7 +314,7 @@ mod tests { test_add_movie_select_minimuum_availability_scroll, AddMovieHandler, MinimumAvailability, - add_movie_minimum_availability_list, + movie_minimum_availability_list, ActiveRadarrBlock::AddMovieSelectMinimumAvailability, None ); @@ -339,7 +322,7 @@ mod tests { test_iterable_scroll!( test_add_movie_select_quality_profile_scroll, AddMovieHandler, - add_movie_quality_profile_list, + movie_quality_profile_list, ActiveRadarrBlock::AddMovieSelectQualityProfile, None ); @@ -389,7 +372,7 @@ mod tests { test_add_movie_select_monitor_home_end, AddMovieHandler, Monitor, - add_movie_monitor_list, + movie_monitor_list, ActiveRadarrBlock::AddMovieSelectMonitor, None ); @@ -398,7 +381,7 @@ mod tests { test_add_movie_select_minimuum_availability_home_end, AddMovieHandler, MinimumAvailability, - add_movie_minimum_availability_list, + movie_minimum_availability_list, ActiveRadarrBlock::AddMovieSelectMinimumAvailability, None ); @@ -406,7 +389,7 @@ mod tests { test_iterable_home_and_end!( test_add_movie_select_quality_profile_scroll, AddMovieHandler, - add_movie_quality_profile_list, + movie_quality_profile_list, ActiveRadarrBlock::AddMovieSelectQualityProfile, None ); @@ -488,24 +471,28 @@ mod tests { app.get_current_route(), &ActiveRadarrBlock::AddMoviePrompt.into() ); - assert!(!app.data.radarr_data.add_movie_monitor_list.items.is_empty()); + assert_eq!( + app.data.radarr_data.selected_block, + ActiveRadarrBlock::AddMovieSelectMonitor + ); + assert!(!app.data.radarr_data.movie_monitor_list.items.is_empty()); assert!(!app .data .radarr_data - .add_movie_minimum_availability_list + .movie_minimum_availability_list .items .is_empty()); assert!(!app .data .radarr_data - .add_movie_quality_profile_list + .movie_quality_profile_list .items .is_empty()); assert_str_eq!( app .data .radarr_data - .add_movie_quality_profile_list + .movie_quality_profile_list .current_selection(), "A - Test 1" ); @@ -558,7 +545,7 @@ mod tests { } #[test] - fn test_add_movie_prompt_prompt_decline() { + fn test_add_movie_prompt_prompt_decline_submit() { let mut app = App::default(); app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); @@ -577,7 +564,7 @@ mod tests { } #[test] - fn test_add_movie_confirm_prompt_prompt_confirmation() { + fn test_add_movie_confirm_prompt_prompt_confirmation_submit() { let mut app = App::default(); app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); app.push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); @@ -600,7 +587,7 @@ mod tests { } #[rstest] - fn test_add_movie_prompt_selected_block( + fn test_add_movie_prompt_selected_block_submit( #[values( ActiveRadarrBlock::AddMovieSelectMonitor, ActiveRadarrBlock::AddMovieSelectMinimumAvailability, @@ -634,7 +621,7 @@ mod tests { } #[rstest] - fn test_add_movie_prompt_selecting_preferences_blocks( + fn test_add_movie_prompt_selecting_preferences_blocks_submit( #[values( ActiveRadarrBlock::AddMovieSelectMonitor, ActiveRadarrBlock::AddMovieSelectMinimumAvailability, @@ -667,7 +654,7 @@ mod tests { use crate::app::radarr::radarr_test_utils::create_test_radarr_data; use crate::{ - assert_add_movie_selections_reset, assert_search_reset, simple_stateful_iterable_vec, + assert_movie_preferences_selections_reset, assert_search_reset, simple_stateful_iterable_vec, }; use super::*; @@ -765,7 +752,7 @@ mod tests { app.get_current_route(), &ActiveRadarrBlock::AddMovieSearchResults.into() ); - assert_add_movie_selections_reset!(app.data.radarr_data); + assert_movie_preferences_selections_reset!(app.data.radarr_data); } #[rstest] diff --git a/src/handlers/radarr_handlers/collection_details_handler.rs b/src/handlers/radarr_handlers/collection_details_handler.rs index f85681e..3f9c125 100644 --- a/src/handlers/radarr_handlers/collection_details_handler.rs +++ b/src/handlers/radarr_handlers/collection_details_handler.rs @@ -94,11 +94,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for CollectionDetailsHandler<'a> ) .into(), ); - self - .app - .data - .radarr_data - .populate_add_movie_preferences_lists(); + self.app.data.radarr_data.populate_movie_preferences_lists(); } } } @@ -204,24 +200,24 @@ mod tests { ) .into() ); - assert!(!app.data.radarr_data.add_movie_monitor_list.items.is_empty()); + assert!(!app.data.radarr_data.movie_monitor_list.items.is_empty()); assert!(!app .data .radarr_data - .add_movie_minimum_availability_list + .movie_minimum_availability_list .items .is_empty()); assert!(!app .data .radarr_data - .add_movie_quality_profile_list + .movie_quality_profile_list .items .is_empty()); assert_str_eq!( app .data .radarr_data - .add_movie_quality_profile_list + .movie_quality_profile_list .current_selection(), "A - Test 1" ); diff --git a/src/handlers/radarr_handlers/edit_movie_handler.rs b/src/handlers/radarr_handlers/edit_movie_handler.rs new file mode 100644 index 0000000..41e722b --- /dev/null +++ b/src/handlers/radarr_handlers/edit_movie_handler.rs @@ -0,0 +1,661 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::radarr::ActiveRadarrBlock; +use crate::app::App; +use crate::event::Key; +use crate::handle_text_box_keys; +use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::Scrollable; +use crate::network::radarr_network::RadarrEvent; + +pub(super) struct EditMovieHandler<'a> { + key: &'a Key, + app: &'a mut App, + active_radarr_block: &'a ActiveRadarrBlock, + context: &'a Option, +} + +impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditMovieHandler<'a> { + fn with( + key: &'a Key, + app: &'a mut App, + active_block: &'a ActiveRadarrBlock, + context: &'a Option, + ) -> EditMovieHandler<'a> { + EditMovieHandler { + 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::EditMovieSelectMinimumAvailability => self + .app + .data + .radarr_data + .movie_minimum_availability_list + .scroll_up(), + ActiveRadarrBlock::EditMovieSelectQualityProfile => self + .app + .data + .radarr_data + .movie_quality_profile_list + .scroll_up(), + ActiveRadarrBlock::EditMoviePrompt => { + self.app.data.radarr_data.selected_block = self + .app + .data + .radarr_data + .selected_block + .clone() + .previous_edit_prompt_block() + } + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::EditMovieSelectMinimumAvailability => self + .app + .data + .radarr_data + .movie_minimum_availability_list + .scroll_down(), + ActiveRadarrBlock::EditMovieSelectQualityProfile => self + .app + .data + .radarr_data + .movie_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() + } + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::EditMovieSelectMinimumAvailability => self + .app + .data + .radarr_data + .movie_minimum_availability_list + .scroll_to_top(), + ActiveRadarrBlock::EditMovieSelectQualityProfile => self + .app + .data + .radarr_data + .movie_quality_profile_list + .scroll_to_top(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::EditMovieSelectMinimumAvailability => self + .app + .data + .radarr_data + .movie_minimum_availability_list + .scroll_to_bottom(), + ActiveRadarrBlock::EditMovieSelectQualityProfile => self + .app + .data + .radarr_data + .movie_quality_profile_list + .scroll_to_bottom(), + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if let ActiveRadarrBlock::EditMoviePrompt = self.active_radarr_block { + handle_prompt_toggle(self.app, self.key) + } + } + + fn handle_submit(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::EditMoviePrompt => match self.app.data.radarr_data.selected_block { + ActiveRadarrBlock::EditMovieConfirmPrompt => { + if self.app.data.radarr_data.prompt_confirm { + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::EditMovie); + self.app.pop_navigation_stack(); + } else { + self.app.pop_navigation_stack(); + } + } + ActiveRadarrBlock::EditMovieSelectMinimumAvailability + | ActiveRadarrBlock::EditMovieSelectQualityProfile => self + .app + .push_navigation_stack((self.app.data.radarr_data.selected_block, *self.context).into()), + ActiveRadarrBlock::EditMoviePathInput | ActiveRadarrBlock::EditMovieTagsInput => { + self.app.push_navigation_stack( + (self.app.data.radarr_data.selected_block, *self.context).into(), + ); + self.app.should_ignore_quit_key = true; + } + ActiveRadarrBlock::EditMovieToggleMonitored => { + self.app.data.radarr_data.edit_monitored = + Some(!self.app.data.radarr_data.edit_monitored.unwrap_or_default()) + } + _ => (), + }, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability + | ActiveRadarrBlock::EditMovieSelectQualityProfile => self.app.pop_navigation_stack(), + ActiveRadarrBlock::EditMoviePathInput | ActiveRadarrBlock::EditMovieTagsInput => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::EditMovieTagsInput | ActiveRadarrBlock::EditMoviePathInput => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + } + ActiveRadarrBlock::EditMoviePrompt => { + self.app.pop_navigation_stack(); + self.app.data.radarr_data.reset_edit_movie(); + self.app.data.radarr_data.prompt_confirm = false; + } + ActiveRadarrBlock::EditMovieToggleMonitored + | ActiveRadarrBlock::EditMovieSelectMinimumAvailability + | ActiveRadarrBlock::EditMovieSelectQualityProfile => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_radarr_block { + ActiveRadarrBlock::EditMoviePathInput => { + handle_text_box_keys!(self, key, self.app.data.radarr_data.edit_path) + } + ActiveRadarrBlock::EditMovieTagsInput => { + handle_text_box_keys!(self, key, self.app.data.radarr_data.edit_tags) + } + _ => (), + } + } +} + +#[cfg(test)] +#[allow(unused_imports)] +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_movie_handler::EditMovieHandler; + use crate::handlers::KeyEventHandler; + use crate::models::radarr_models::{MinimumAvailability, Monitor}; + + 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_movie_select_minimuum_availability_scroll, + EditMovieHandler, + MinimumAvailability, + movie_minimum_availability_list, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + None + ); + + test_iterable_scroll!( + test_edit_movie_select_quality_profile_scroll, + EditMovieHandler, + movie_quality_profile_list, + ActiveRadarrBlock::EditMovieSelectQualityProfile, + None + ); + + #[rstest] + fn test_edit_movie_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.data.radarr_data.selected_block = ActiveRadarrBlock::EditMovieSelectMinimumAvailability; + + EditMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::EditMoviePrompt, &None).handle(); + + if key == Key::Up { + assert_eq!( + app.data.radarr_data.selected_block, + ActiveRadarrBlock::EditMovieToggleMonitored + ); + } else { + assert_eq!( + app.data.radarr_data.selected_block, + ActiveRadarrBlock::EditMovieSelectQualityProfile + ); + } + } + } + + mod test_handle_home_end { + use strum::IntoEnumIterator; + + use crate::{test_enum_home_and_end, test_iterable_home_and_end}; + + use super::*; + + test_enum_home_and_end!( + test_edit_movie_select_minimuum_availability_home_end, + EditMovieHandler, + MinimumAvailability, + movie_minimum_availability_list, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + None + ); + + test_iterable_home_and_end!( + test_edit_movie_select_quality_profile_scroll, + EditMovieHandler, + movie_quality_profile_list, + ActiveRadarrBlock::EditMovieSelectQualityProfile, + None + ); + } + + mod test_handle_left_right_action { + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::default(); + + EditMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::EditMoviePrompt, &None).handle(); + + assert!(app.data.radarr_data.prompt_confirm); + + EditMovieHandler::with(&key, &mut app, &ActiveRadarrBlock::EditMoviePrompt, &None).handle(); + + assert!(!app.data.radarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use std::collections::HashMap; + + use pretty_assertions::{assert_eq, assert_str_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_movie_path_input_submit() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.data.radarr_data.edit_path = "Test Path".to_owned(); + app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditMoviePathInput.into()); + + EditMovieHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditMoviePathInput, + &None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app.data.radarr_data.edit_path.is_empty()); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditMoviePrompt.into() + ); + } + + #[test] + fn test_edit_movie_tags_input_submit() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.data.radarr_data.edit_tags = "Test Tags".to_owned(); + app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditMoviePathInput.into()); + + EditMovieHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditMovieTagsInput, + &None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app.data.radarr_data.edit_tags.is_empty()); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditMoviePrompt.into() + ); + } + + #[test] + fn test_edit_movie_prompt_prompt_decline_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); + app.data.radarr_data.selected_block = ActiveRadarrBlock::EditMovieConfirmPrompt; + + EditMovieHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditMoviePrompt, + &None, + ) + .handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!(app.data.radarr_data.prompt_confirm_action, None); + } + + #[test] + fn test_edit_movie_confirm_prompt_prompt_confirmation_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); + app.data.radarr_data.prompt_confirm = true; + app.data.radarr_data.selected_block = ActiveRadarrBlock::EditMovieConfirmPrompt; + + EditMovieHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditMoviePrompt, + &None, + ) + .handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + assert_eq!( + app.data.radarr_data.prompt_confirm_action, + Some(RadarrEvent::EditMovie) + ); + } + + #[test] + fn test_edit_movie_toggle_monitored_submit() { + let current_route = Route::from(( + ActiveRadarrBlock::EditMoviePrompt, + Some(ActiveRadarrBlock::Movies), + )); + let mut app = App::default(); + app.data.radarr_data.selected_block = ActiveRadarrBlock::EditMovieToggleMonitored; + app.push_navigation_stack(current_route); + + EditMovieHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditMoviePrompt, + &Some(ActiveRadarrBlock::Movies), + ) + .handle(); + + assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.data.radarr_data.edit_monitored, Some(true)); + + EditMovieHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditMoviePrompt, + &Some(ActiveRadarrBlock::Movies), + ) + .handle(); + + assert_eq!(app.get_current_route(), ¤t_route); + assert_eq!(app.data.radarr_data.edit_monitored, Some(false)); + } + + #[rstest] + fn test_edit_movie_prompt_selected_block_submit( + #[values( + ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + ActiveRadarrBlock::EditMovieSelectQualityProfile, + ActiveRadarrBlock::EditMoviePathInput, + ActiveRadarrBlock::EditMovieTagsInput + )] + selected_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack( + ( + ActiveRadarrBlock::EditMoviePrompt, + Some(ActiveRadarrBlock::Movies), + ) + .into(), + ); + app.data.radarr_data.selected_block = selected_block; + + EditMovieHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditMoviePrompt, + &Some(ActiveRadarrBlock::Movies), + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &(selected_block, Some(ActiveRadarrBlock::Movies)).into() + ); + assert_eq!(app.data.radarr_data.prompt_confirm_action, None); + + if selected_block == ActiveRadarrBlock::EditMoviePathInput + || selected_block == ActiveRadarrBlock::EditMovieTagsInput + { + assert!(app.should_ignore_quit_key); + } + } + + #[rstest] + fn test_edit_movie_prompt_selecting_preferences_blocks_submit( + #[values( + ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + ActiveRadarrBlock::EditMovieSelectQualityProfile, + ActiveRadarrBlock::EditMoviePathInput, + ActiveRadarrBlock::EditMovieTagsInput + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); + app.push_navigation_stack(active_radarr_block.into()); + + EditMovieHandler::with( + &SUBMIT_KEY, + &mut app, + &active_radarr_block, + &Some(ActiveRadarrBlock::Movies), + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditMoviePrompt.into() + ); + + if active_radarr_block == ActiveRadarrBlock::EditMoviePathInput + || active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput + { + 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_movie_reset, assert_movie_preferences_selections_reset}; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_edit_movie_input_esc( + #[values( + ActiveRadarrBlock::EditMovieTagsInput, + ActiveRadarrBlock::EditMoviePathInput + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.data.radarr_data = create_test_radarr_data(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into()); + app.push_navigation_stack(active_radarr_block.into()); + + EditMovieHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); + + assert!(!app.should_ignore_quit_key); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::AddMovieSearchInput.into() + ); + } + + #[test] + fn test_edit_movie_prompt_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into()); + app.data.radarr_data = create_test_radarr_data(); + + EditMovieHandler::with( + &ESC_KEY, + &mut app, + &ActiveRadarrBlock::EditMoviePrompt, + &None, + ) + .handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + { + 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); + } + } + + #[rstest] + fn test_edit_movie_esc( + #[values( + ActiveRadarrBlock::EditMovieToggleMonitored, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + ActiveRadarrBlock::EditMovieSelectQualityProfile + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.data.radarr_data = create_test_radarr_data(); + app.push_navigation_stack(active_radarr_block.into()); + + EditMovieHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + } + } + + mod test_handle_key_char { + use super::*; + + #[test] + fn test_edit_movie_path_input_backspace() { + let mut app = App::default(); + app.data.radarr_data.edit_path = "Test".to_owned(); + + EditMovieHandler::with( + &DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + &ActiveRadarrBlock::EditMoviePathInput, + &None, + ) + .handle(); + + assert_str_eq!(app.data.radarr_data.edit_path, "Tes"); + } + + #[test] + fn test_edit_movie_tags_input_backspace() { + let mut app = App::default(); + app.data.radarr_data.edit_tags = "Test".to_owned(); + + EditMovieHandler::with( + &DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + &ActiveRadarrBlock::EditMovieTagsInput, + &None, + ) + .handle(); + + assert_str_eq!(app.data.radarr_data.edit_tags, "Tes"); + } + + #[test] + fn test_edit_movie_path_input_char_key() { + let mut app = App::default(); + + EditMovieHandler::with( + &Key::Char('h'), + &mut app, + &ActiveRadarrBlock::EditMoviePathInput, + &None, + ) + .handle(); + + assert_str_eq!(app.data.radarr_data.edit_path, "h"); + } + + #[test] + fn test_edit_movie_tags_input_char_key() { + let mut app = App::default(); + + EditMovieHandler::with( + &Key::Char('h'), + &mut app, + &ActiveRadarrBlock::EditMovieTagsInput, + &None, + ) + .handle(); + + assert_str_eq!(app.data.radarr_data.edit_tags, "h"); + } + } +} diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index aab0332..0ffae8a 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, FILTER_BLOCKS, + ActiveRadarrBlock, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_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_movie_handler::EditMovieHandler; use crate::handlers::radarr_handlers::movie_details_handler::MovieDetailsHandler; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::models::Scrollable; @@ -14,6 +15,7 @@ use crate::{handle_text_box_keys, App, Key}; mod add_movie_handler; mod collection_details_handler; +mod edit_movie_handler; mod movie_details_handler; pub(super) struct RadarrHandler<'a> { @@ -37,6 +39,9 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { _ if ADD_MOVIE_BLOCKS.contains(self.active_radarr_block) => { AddMovieHandler::with(self.key, self.app, self.active_radarr_block, self.context).handle() } + _ if EDIT_MOVIE_BLOCKS.contains(self.active_radarr_block) => { + EditMovieHandler::with(self.key, self.app, self.active_radarr_block, self.context).handle() + } _ => self.handle_key_event(), } } @@ -401,6 +406,17 @@ 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::EditMoviePrompt, + Some(ActiveRadarrBlock::Movies), + ) + .into(), + ); + self.app.data.radarr_data.populate_edit_movie_fields(); + self.app.data.radarr_data.selected_block = ActiveRadarrBlock::EditMovieToggleMonitored; + } _ if *key == DEFAULT_KEYBINDINGS.add.key => { self .app @@ -513,6 +529,76 @@ impl RadarrHandler<'_> { } } +#[cfg(test)] +#[macro_use] +mod radarr_handler_test_utils { + #[macro_export] + macro_rules! test_edit_movie_key { + ($handler:ident, $block:expr, $context:expr) => { + let mut app = App::default(); + let mut radarr_data = RadarrData { + edit_path: String::default(), + edit_tags: String::default(), + edit_monitored: None, + quality_profile_map: HashMap::from([ + (2222, "HD - 1080p".to_owned()), + (1111, "Any".to_owned()), + ]), + filtered_movies: StatefulTable::default(), + ..create_test_radarr_data() + }; + radarr_data.movies.set_items(vec![Movie { + path: "/nfs/movies/Test".to_owned(), + monitored: true, + quality_profile_id: Number::from(2222), + minimum_availability: MinimumAvailability::Released, + ..Movie::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::EditMoviePrompt, Some($context)).into() + ); + assert_eq!( + app.data.radarr_data.selected_block, + ActiveRadarrBlock::EditMovieToggleMonitored + ); + assert_eq!( + app.data.radarr_data.movie_minimum_availability_list.items, + Vec::from_iter(MinimumAvailability::iter()) + ); + assert_eq!( + app + .data + .radarr_data + .movie_minimum_availability_list + .current_selection(), + &MinimumAvailability::Released + ); + assert_eq!( + app.data.radarr_data.movie_quality_profile_list.items, + vec!["Any".to_owned(), "HD - 1080p".to_owned()] + ); + assert_eq!( + app + .data + .radarr_data + .movie_quality_profile_list + .current_selection(), + "HD - 1080p" + ); + assert_eq!( + app.data.radarr_data.edit_path, + "/nfs/movies/Test".to_owned() + ); + assert_eq!(app.data.radarr_data.edit_monitored, Some(true)); + }; + } +} + #[cfg(test)] mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; @@ -1122,10 +1208,18 @@ mod tests { } mod test_handle_key_char { + use std::collections::HashMap; + use pretty_assertions::assert_eq; use rstest::rstest; + use serde_json::Number; + use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::radarr::radarr_test_utils::create_test_radarr_data; + use crate::app::radarr::RadarrData; + use crate::models::radarr_models::MinimumAvailability; + use crate::models::StatefulTable; use super::*; @@ -1192,6 +1286,15 @@ mod tests { assert!(app.should_ignore_quit_key); } + #[test] + fn test_movie_edit_key() { + test_edit_movie_key!( + RadarrHandler, + ActiveRadarrBlock::Movies, + ActiveRadarrBlock::Movies + ); + } + #[rstest] #[case(ActiveRadarrBlock::Movies, ActiveRadarrBlock::RefreshAllMoviesPrompt)] #[case( @@ -1453,4 +1556,19 @@ mod tests { ) { test_handler_delegation!(ActiveRadarrBlock::Movies, active_radarr_block); } + + #[rstest] + fn test_delegate_edit_movie_blocks_to_edit_movie_handler( + #[values( + ActiveRadarrBlock::EditMoviePrompt, + ActiveRadarrBlock::EditMoviePathInput, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + ActiveRadarrBlock::EditMovieSelectQualityProfile, + ActiveRadarrBlock::EditMovieTagsInput, + ActiveRadarrBlock::EditMovieToggleMonitored + )] + active_radarr_block: ActiveRadarrBlock, + ) { + test_handler_delegation!(ActiveRadarrBlock::Movies, active_radarr_block); + } } diff --git a/src/handlers/radarr_handlers/movie_details_handler.rs b/src/handlers/radarr_handlers/movie_details_handler.rs index c999105..8c120db 100644 --- a/src/handlers/radarr_handlers/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/movie_details_handler.rs @@ -225,6 +225,17 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for MovieDetailsHandler<'a> { .app .push_navigation_stack(ActiveRadarrBlock::AutomaticallySearchMoviePrompt.into()); } + _ if *key == DEFAULT_KEYBINDINGS.edit.key => { + self.app.push_navigation_stack( + ( + ActiveRadarrBlock::EditMoviePrompt, + Some(*self.active_radarr_block), + ) + .into(), + ); + self.app.data.radarr_data.populate_edit_movie_fields(); + self.app.data.radarr_data.selected_block = ActiveRadarrBlock::EditMovieToggleMonitored; + } _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { self .app @@ -762,8 +773,18 @@ mod tests { } mod test_handle_key_char { + use std::collections::HashMap; + use pretty_assertions::assert_eq; use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::radarr::radarr_test_utils::create_test_radarr_data; + use crate::app::radarr::RadarrData; + use crate::handlers::radarr_handlers::RadarrHandler; + use crate::models::radarr_models::{MinimumAvailability, Movie}; + use crate::models::StatefulTable; + use crate::test_edit_movie_key; use super::*; @@ -816,6 +837,25 @@ mod tests { assert_eq!(app.data.radarr_data.sort_ascending, Some(false)); } + #[rstest] + fn test_edit_key( + #[values( + ActiveRadarrBlock::MovieDetails, + ActiveRadarrBlock::MovieHistory, + ActiveRadarrBlock::FileInfo, + ActiveRadarrBlock::Cast, + ActiveRadarrBlock::Crew, + ActiveRadarrBlock::ManualSearch + )] + active_radarr_block: ActiveRadarrBlock, + ) { + test_edit_movie_key!( + MovieDetailsHandler, + active_radarr_block, + active_radarr_block + ); + } + #[rstest] fn test_refresh_key( #[values( diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 783ced3..5772544 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -55,6 +55,7 @@ pub struct Movie { pub tmdb_id: Number, #[derivative(Default(value = "Number::from(0)"))] pub quality_profile_id: Number, + pub minimum_availability: MinimumAvailability, pub certification: Option, pub ratings: RatingsList, pub movie_file: Option, @@ -312,7 +313,8 @@ pub struct CommandBody { pub name: String, } -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter)] +#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter)] +#[serde(rename_all = "lowercase")] pub enum MinimumAvailability { #[default] Announced, diff --git a/src/network/mod.rs b/src/network/mod.rs index 5476262..8fb9fbb 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -67,7 +67,7 @@ impl<'a> Network<'a> { .handle_error(anyhow!("Failed to parse response! {:?}", e)); } }, - RequestMethod::Delete | RequestMethod::Post => (), + RequestMethod::Delete | RequestMethod::Post | RequestMethod::Put => (), } } else { error!( @@ -115,6 +115,11 @@ impl<'a> Network<'a> { .post(uri) .json(&body.unwrap_or_default()) .header("X-Api-Key", api_token), + RequestMethod::Put => app + .client + .put(uri) + .json(&body.unwrap_or_default()) + .header("X-Api-Key", api_token), RequestMethod::Delete => app.client.delete(uri).header("X-Api-Key", api_token), } } @@ -124,6 +129,7 @@ impl<'a> Network<'a> { pub enum RequestMethod { Get, Post, + Put, Delete, } @@ -188,7 +194,8 @@ mod tests { #[rstest] #[tokio::test] async fn test_handle_request_no_response_body( - #[values(RequestMethod::Post, RequestMethod::Delete)] request_method: RequestMethod, + #[values(RequestMethod::Post, RequestMethod::Put, RequestMethod::Delete)] + request_method: RequestMethod, ) { let mut server = Server::new_async().await; let async_server = server @@ -226,7 +233,6 @@ mod tests { .handle_request::<(), Test>( RequestProps { uri: format!("{}/test", server.url()), - // uri: format!("{}/test", url), method: RequestMethod::Get, body: None, api_token: "test1234".to_owned(), @@ -301,7 +307,12 @@ mod tests { #[rstest] #[tokio::test] async fn test_handle_request_non_success_code( - #[values(RequestMethod::Get, RequestMethod::Post, RequestMethod::Delete)] + #[values( + RequestMethod::Get, + RequestMethod::Post, + RequestMethod::Put, + RequestMethod::Delete + )] request_method: RequestMethod, ) { let (async_server, app_arc, server) = mock_api(request_method, 404, true).await; @@ -329,7 +340,12 @@ mod tests { #[rstest] #[tokio::test] async fn test_call_api( - #[values(RequestMethod::Get, RequestMethod::Post, RequestMethod::Delete)] + #[values( + RequestMethod::Get, + RequestMethod::Post, + RequestMethod::Put, + RequestMethod::Delete + )] request_method: RequestMethod, ) { let mut server = Server::new_async().await; diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 23d982f..6ddfbe8 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -3,16 +3,17 @@ use std::fmt::Debug; use indoc::formatdoc; use log::{debug, info}; use serde::Serialize; -use serde_json::Number; +use serde_json::{json, Number, Value}; use urlencoding::encode; +use crate::app::radarr::ActiveRadarrBlock; use crate::app::RadarrConfig; use crate::models::radarr_models::{ - AddMovieBody, AddMovieSearchResult, AddOptions, Collection, CommandBody, Credit, CreditType, - DiskSpace, DownloadRecord, DownloadsResponse, Movie, MovieCommandBody, MovieHistoryItem, - QualityProfile, Release, ReleaseDownloadBody, RootFolder, SystemStatus, + AddMovieBody, AddMovieSearchResult, AddOptions, Collection, CollectionMovie, CommandBody, Credit, + CreditType, DiskSpace, DownloadRecord, DownloadsResponse, Movie, MovieCommandBody, + MovieHistoryItem, QualityProfile, Release, ReleaseDownloadBody, RootFolder, SystemStatus, }; -use crate::models::ScrollableText; +use crate::models::{Route, ScrollableText}; use crate::network::{Network, NetworkEvent, RequestMethod, RequestProps}; use crate::utils::{convert_runtime, convert_to_gb}; @@ -22,6 +23,7 @@ pub enum RadarrEvent { DeleteDownload, DeleteMovie, DownloadRelease, + EditMovie, GetCollections, GetDownloads, GetMovieCredits, @@ -48,6 +50,7 @@ impl RadarrEvent { RadarrEvent::GetCollections => "/collection", RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue", RadarrEvent::AddMovie + | RadarrEvent::EditMovie | RadarrEvent::GetMovies | RadarrEvent::GetMovieDetails | RadarrEvent::DeleteMovie => "/movie", @@ -82,6 +85,7 @@ impl<'a> Network<'a> { RadarrEvent::DeleteMovie => self.delete_movie().await, RadarrEvent::DeleteDownload => self.delete_download().await, RadarrEvent::DownloadRelease => self.download_release().await, + RadarrEvent::EditMovie => self.edit_movie().await, RadarrEvent::GetCollections => self.get_collections().await, RadarrEvent::GetDownloads => self.get_downloads().await, RadarrEvent::GetMovieCredits => self.get_credits().await, @@ -675,11 +679,31 @@ impl<'a> Network<'a> { let body = { let app = self.app.lock().await; let root_folders = app.data.radarr_data.root_folders.to_vec(); - let current_selection = app - .data - .radarr_data - .add_searched_movies - .current_selection_clone(); + let (tmdb_id, title) = if let Route::Radarr(active_radarr_block, _) = app.get_current_route() + { + if *active_radarr_block == ActiveRadarrBlock::CollectionDetails { + let CollectionMovie { tmdb_id, title, .. } = app + .data + .radarr_data + .collection_movies + .current_selection_clone(); + (tmdb_id, title.stationary_style()) + } else { + let AddMovieSearchResult { tmdb_id, title, .. } = app + .data + .radarr_data + .add_searched_movies + .current_selection_clone(); + (tmdb_id, title.stationary_style()) + } + } else { + let AddMovieSearchResult { tmdb_id, title, .. } = app + .data + .radarr_data + .add_searched_movies + .current_selection_clone(); + (tmdb_id, title.stationary_style()) + }; let quality_profile_map = app.data.radarr_data.quality_profile_map.clone(); let RootFolder { path, .. } = root_folders @@ -696,21 +720,20 @@ impl<'a> Network<'a> { let monitor = app .data .radarr_data - .add_movie_monitor_list + .movie_monitor_list .current_selection() .to_string(); let minimum_availability = app .data .radarr_data - .add_movie_minimum_availability_list + .movie_minimum_availability_list .current_selection() .to_string(); let quality_profile = app .data .radarr_data - .add_movie_quality_profile_list + .movie_quality_profile_list .current_selection_clone(); - let AddMovieSearchResult { tmdb_id, title, .. } = current_selection; let quality_profile_id = quality_profile_map .iter() .filter(|(_, value)| **value == quality_profile) @@ -720,7 +743,7 @@ impl<'a> Network<'a> { AddMovieBody { tmdb_id: tmdb_id.as_u64().unwrap(), - title: title.stationary_style(), + title, root_folder_path: path.to_owned(), minimum_availability, monitored: true, @@ -747,6 +770,80 @@ impl<'a> Network<'a> { .await; } + async fn edit_movie(&self) { + info!("Editing Radarr movie"); + + info!("Fetching movie details"); + let movie_id = self.extract_movie_id().await; + let request_props = self + .radarr_request_props_from( + format!("{}/{}", RadarrEvent::GetMovieDetails.resource(), movie_id).as_str(), + RequestMethod::Get, + None::<()>, + ) + .await; + + self + .handle_request::<(), Value>(request_props, |detailed_movie_body, mut app| { + app.data.radarr_data.movie_details = + ScrollableText::with_string(detailed_movie_body.to_string()) + }) + .await; + + info!("Constructing edit movie body"); + + let body = { + let mut app = self.app.lock().await; + let mut detailed_movie_body: Value = + serde_json::from_str(&app.data.radarr_data.movie_details.get_text()).unwrap(); + app.data.radarr_data.movie_details = ScrollableText::default(); + + let quality_profile_map = app.data.radarr_data.quality_profile_map.clone(); + let path: String = app.data.radarr_data.edit_path.drain(..).collect(); + let _tags: String = app.data.radarr_data.edit_tags.drain(..).collect(); + + let monitored = app.data.radarr_data.edit_monitored.unwrap_or_default(); + let minimum_availability = app + .data + .radarr_data + .movie_minimum_availability_list + .current_selection() + .to_string(); + let quality_profile = app + .data + .radarr_data + .movie_quality_profile_list + .current_selection_clone(); + let quality_profile_id = quality_profile_map + .iter() + .filter(|(_, value)| **value == quality_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + + *detailed_movie_body.get_mut("monitored").unwrap() = json!(monitored); + *detailed_movie_body.get_mut("minimumAvailability").unwrap() = json!(minimum_availability); + *detailed_movie_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id); + *detailed_movie_body.get_mut("path").unwrap() = json!(path); + + detailed_movie_body + }; + + debug!("Edit movie body: {:?}", body); + + let request_props = self + .radarr_request_props_from( + format!("{}/{}", RadarrEvent::EditMovie.resource(), movie_id).as_str(), + RequestMethod::Put, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + async fn download_release(&self) { let Release { guid, @@ -883,6 +980,7 @@ mod test { use strum::IntoEnumIterator; use tokio::sync::Mutex; + use crate::app::radarr::ActiveRadarrBlock; use crate::models::radarr_models::{ CollectionMovie, Language, MediaInfo, MinimumAvailability, Monitor, MovieFile, Quality, QualityWrapper, Rating, RatingsList, @@ -910,6 +1008,7 @@ mod test { "hasFile": true, "runtime": 120, "qualityProfileId": 2222, + "minimumAvailability": "announced", "certification": "R", "ratings": { "imdb": { @@ -1194,7 +1293,7 @@ mod test { #[tokio::test] async fn test_handle_search_new_movie_event() { let add_movie_search_result_json = json!([{ - "tmdbId": 1, + "tmdbId": 1234, "title": "Test", "originalLanguage": { "name": "English" }, "status": "released", @@ -1466,6 +1565,7 @@ mod test { "runtime": 120, "tmdbId": 1234, "qualityProfileId": 2222, + "minimumAvailability": "released", "ratings": {} }); let (async_server, app_arc, _server) = mock_radarr_api( @@ -1803,12 +1903,13 @@ mod test { async_server.assert_async().await; } + #[rstest] #[tokio::test] - async fn test_handle_add_movie_event() { + async fn test_handle_add_movie_event(#[values(true, false)] collection_details_context: bool) { let (async_server, app_arc, _server) = mock_radarr_api( RequestMethod::Post, Some(json!({ - "tmdbId": 1, + "tmdbId": 1234, "title": "Test", "rootFolderPath": "/nfs", "minimumAvailability": "announced", @@ -1838,27 +1939,36 @@ mod test { free_space: Number::from(21990232555520u64), }, ]; - app - .data - .radarr_data - .add_searched_movies - .set_items(vec![add_movie_search_result()]); app.data.radarr_data.quality_profile_map = HashMap::from([(2222, "HD - 1080p".to_owned())]); app .data .radarr_data - .add_movie_quality_profile_list + .movie_quality_profile_list .set_items(vec!["HD - 1080p".to_owned()]); app .data .radarr_data - .add_movie_monitor_list + .movie_monitor_list .set_items(Vec::from_iter(Monitor::iter())); app .data .radarr_data - .add_movie_minimum_availability_list + .movie_minimum_availability_list .set_items(Vec::from_iter(MinimumAvailability::iter())); + if collection_details_context { + app + .data + .radarr_data + .collection_movies + .set_items(vec![collection_movie()]); + app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()); + } else { + app + .data + .radarr_data + .add_searched_movies + .set_items(vec![add_movie_search_result()]); + } } let network = Network::new(reqwest::Client::new(), &app_arc); @@ -1867,6 +1977,68 @@ mod test { async_server.assert_async().await; } + #[tokio::test] + async fn test_handle_edit_movie_event() { + let mut expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); + *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("path").unwrap() = json!("/nfs/Test Path"); + + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(serde_json::from_str(MOVIE_JSON).unwrap()), + format!("{}/1", RadarrEvent::GetMovieDetails.resource()).as_str(), + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1", RadarrEvent::EditMovie.resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + { + let mut app = app_arc.lock().await; + app.data.radarr_data.edit_tags = "test tag".to_owned(); + app.data.radarr_data.edit_path = "/nfs/Test Path".to_owned(); + app.data.radarr_data.edit_monitored = Some(false); + app + .data + .radarr_data + .movie_quality_profile_list + .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); + app + .data + .radarr_data + .movie_minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + app.data.radarr_data.movies.set_items(vec![Movie { + monitored: false, + ..movie() + }]); + app.data.radarr_data.quality_profile_map = + HashMap::from([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); + } + let network = Network::new(reqwest::Client::new(), &app_arc); + + network.handle_radarr_event(RadarrEvent::EditMovie).await; + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + + { + let app = app_arc.lock().await; + assert!(app.data.radarr_data.edit_path.is_empty()); + assert!(app.data.radarr_data.edit_tags.is_empty()); + assert!(app.data.radarr_data.movie_details.items.is_empty()); + } + } + #[tokio::test] async fn test_handle_download_release_event() { let (async_server, app_arc, _server) = mock_radarr_api( @@ -2048,8 +2220,7 @@ mod test { &method.to_string().to_uppercase(), format!("/api/v3{}", resource).as_str(), ) - .match_header("X-Api-Key", "test1234") - .with_status(200); + .match_header("X-Api-Key", "test1234"); if let Some(body) = request_body { async_server = async_server.match_body(Matcher::Json(body)); @@ -2169,6 +2340,7 @@ mod test { runtime: Number::from(120), tmdb_id: Number::from(1234), quality_profile_id: Number::from(2222), + minimum_availability: MinimumAvailability::Announced, certification: Some("R".to_owned()), ratings: ratings_list(), movie_file: Some(movie_file()), @@ -2213,7 +2385,7 @@ mod test { fn add_movie_search_result() -> AddMovieSearchResult { AddMovieSearchResult { - tmdb_id: Number::from(1), + tmdb_id: Number::from(1234), title: HorizontallyScrollableText::from("Test".to_owned()), original_language: language(), status: "released".to_owned(), diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 90754dd..c70244d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -15,8 +15,8 @@ use crate::models::{Route, StatefulList, StatefulTable, TabState}; use crate::ui::utils::{ borderless_block, centered_rect, horizontal_chunks, horizontal_chunks_with_margin, layout_block, layout_block_top_border, layout_button_paragraph, layout_button_paragraph_borderless, - layout_paragraph_borderless, logo_block, style_button_highlight, style_default_bold, - style_failure, style_help, style_highlight, style_primary, style_secondary, + layout_paragraph_borderless, logo_block, show_cursor, style_block_highlight, style_default, + style_default_bold, style_failure, style_help, style_highlight, style_primary, style_secondary, style_system_function, title_block, title_block_centered, vertical_chunks, vertical_chunks_with_margin, }; @@ -57,7 +57,7 @@ pub fn ui(f: &mut Frame, app: &mut App) { draw_header_row(f, app, main_chunks[0]); draw_context_row(f, app, main_chunks[1]); if let Route::Radarr(_, _) = app.get_current_route() { - radarr_ui::draw_radarr_ui(f, app, main_chunks[2]) + radarr_ui::draw_radarr_ui(f, app, main_chunks[2]); } } @@ -106,6 +106,18 @@ fn draw_error(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { f.render_widget(paragraph, area); } +pub fn draw_popup( + f: &mut Frame<'_, B>, + app: &mut App, + popup_fn: fn(&mut Frame<'_, B>, &mut App, Rect), + percent_x: u16, + percent_y: u16, +) { + let popup_area = centered_rect(percent_x, percent_y, f.size()); + f.render_widget(Clear, popup_area); + popup_fn(f, app, popup_area); +} + pub fn draw_popup_over( f: &mut Frame<'_, B>, app: &mut App, @@ -117,9 +129,7 @@ pub fn draw_popup_over( ) { background_fn(f, app, area); - let popup_area = centered_rect(percent_x, percent_y, f.size()); - f.render_widget(Clear, popup_area); - popup_fn(f, app, popup_area); + draw_popup(f, app, popup_fn, percent_x, percent_y); } pub fn draw_prompt_popup_over( @@ -375,6 +385,44 @@ pub fn draw_prompt_box_with_content( draw_button(f, horizontal_chunks[1], "No", !*yes_no_value); } +pub fn draw_checkbox( + f: &mut Frame<'_, B>, + area: Rect, + is_checked: bool, + is_selected: bool, +) { + let check = if is_checked { "✔" } else { "" }; + let label_paragraph = Paragraph::new(Text::from(check)) + .block(layout_block()) + .alignment(Alignment::Center) + .style(style_block_highlight(is_selected).add_modifier(Modifier::BOLD)); + let checkbox_area = Rect { width: 5, ..area }; + + f.render_widget(label_paragraph, checkbox_area); +} + +pub fn draw_checkbox_with_label( + f: &mut Frame<'_, B>, + area: Rect, + label: &str, + is_checked: bool, + is_selected: bool, +) { + let horizontal_chunks = horizontal_chunks( + vec![Constraint::Percentage(50), Constraint::Percentage(50)], + area, + ); + + let label_paragraph = Paragraph::new(Text::from(format!("\n{}: ", label))) + .block(borderless_block()) + .alignment(Alignment::Right) + .style(style_primary()); + + f.render_widget(label_paragraph, horizontal_chunks[0]); + + draw_checkbox(f, horizontal_chunks[1], is_checked, is_selected); +} + pub fn draw_button(f: &mut Frame<'_, B>, area: Rect, label: &str, is_selected: bool) { let label_paragraph = layout_button_paragraph(is_selected, label, Alignment::Center); @@ -398,7 +446,7 @@ pub fn draw_button_with_icon( ); f.render_widget( - layout_block().style(style_button_highlight(is_selected)), + layout_block().style(style_block_highlight(is_selected)), area, ); f.render_widget(label_paragraph, horizontal_chunks[0]); @@ -440,3 +488,63 @@ pub fn draw_drop_down_list<'a, B: Backend, T>( f.render_stateful_widget(list, area, &mut content.state); } + +pub fn draw_text_box( + f: &mut Frame<'_, B>, + text_box_area: Rect, + block_title: Option<&str>, + block_content: &str, + should_show_cursor: bool, + is_selected: bool, +) { + let (block, style) = if let Some(..) = block_title { + (title_block_centered(block_title.unwrap()), style_default()) + } else { + ( + layout_block(), + if should_show_cursor { + style_default() + } else { + style_block_highlight(is_selected) + }, + ) + }; + let search_paragraph = Paragraph::new(Text::from(block_content)) + .style(style) + .block(block); + f.render_widget(search_paragraph, text_box_area); + + if should_show_cursor { + show_cursor(f, text_box_area, block_content); + } +} + +pub fn draw_text_box_with_label( + f: &mut Frame<'_, B>, + area: Rect, + label: &str, + text: &str, + is_selected: bool, + should_show_cursor: bool, +) { + let horizontal_chunks = horizontal_chunks( + vec![Constraint::Percentage(50), Constraint::Percentage(50)], + area, + ); + + let label_paragraph = Paragraph::new(Text::from(format!("\n{}: ", label))) + .block(borderless_block()) + .alignment(Alignment::Right) + .style(style_primary()); + + f.render_widget(label_paragraph, horizontal_chunks[0]); + + draw_text_box( + f, + horizontal_chunks[1], + None, + text, + should_show_cursor, + is_selected, + ); +} diff --git a/src/ui/radarr_ui/add_movie_ui.rs b/src/ui/radarr_ui/add_movie_ui.rs index 3ea6231..1e5770a 100644 --- a/src/ui/radarr_ui/add_movie_ui.rs +++ b/src/ui/radarr_ui/add_movie_ui.rs @@ -8,14 +8,18 @@ use crate::app::radarr::ActiveRadarrBlock; use crate::models::radarr_models::AddMovieSearchResult; use crate::models::Route; use crate::ui::radarr_ui::collection_details_ui::draw_collection_details; +use crate::ui::radarr_ui::{ + draw_select_minimum_availability_popup, draw_select_quality_profile_popup, +}; use crate::ui::utils::{ borderless_block, get_width_from_percentage, horizontal_chunks, layout_block, - layout_paragraph_borderless, show_cursor, style_default, style_help, style_primary, - title_block_centered, vertical_chunks_with_margin, + layout_paragraph_borderless, style_help, style_primary, title_block_centered, + vertical_chunks_with_margin, }; use crate::ui::{ draw_button, draw_drop_down_list, draw_drop_down_menu_button, draw_drop_down_popup, - draw_error_popup, draw_error_popup_over, draw_medium_popup_over, draw_table, TableProps, + draw_error_popup, draw_error_popup_over, draw_medium_popup_over, draw_table, draw_text_box, + TableProps, }; use crate::utils::convert_runtime; use crate::App; @@ -80,14 +84,10 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: ); let block_content = app.data.radarr_data.search.as_str(); - let search_paragraph = Paragraph::new(Text::from(block_content)) - .style(style_default()) - .block(title_block_centered("Add Movie")); - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { match active_radarr_block { ActiveRadarrBlock::AddMovieSearchInput => { - show_cursor(f, chunks[0], block_content); + draw_text_box(f, chunks[0], Some("Add Movie"), block_content, true, false); f.render_widget(layout_block(), chunks[1]); let mut help_text = Text::from(" close"); @@ -124,7 +124,7 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: TableProps { content: &mut app.data.radarr_data.add_searched_movies, table_headers: vec![ - "✓", + "✔", "Title", "Year", "Runtime", @@ -179,7 +179,7 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: .iter() .any(|mov| mov.tmdb_id == movie.tmdb_id) { - "✓" + "✔" } else { "" }; @@ -208,7 +208,7 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: } } - f.render_widget(search_paragraph, chunks[0]); + draw_text_box(f, chunks[0], Some("Add Movie"), block_content, false, false); } fn draw_confirmation_popup(f: &mut Frame<'_, B>, app: &mut App, prompt_area: Rect) { @@ -251,37 +251,11 @@ 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.add_movie_monitor_list, + &mut app.data.radarr_data.movie_monitor_list, |monitor| ListItem::new(monitor.to_display_str().to_owned()), ); } -fn draw_select_minimum_availability_popup( - f: &mut Frame<'_, B>, - app: &mut App, - popup_area: Rect, -) { - draw_drop_down_list( - f, - popup_area, - &mut app.data.radarr_data.add_movie_minimum_availability_list, - |minimum_availability| ListItem::new(minimum_availability.to_display_str().to_owned()), - ); -} - -fn draw_select_quality_profile_popup( - f: &mut Frame<'_, B>, - app: &mut App, - popup_area: Rect, -) { - draw_drop_down_list( - f, - popup_area, - &mut app.data.radarr_data.add_movie_quality_profile_list, - |quality_profile| ListItem::new(quality_profile.clone()), - ); -} - fn draw_confirmation_prompt(f: &mut Frame<'_, B>, app: &mut App, prompt_area: Rect) { let title = "Add Movie"; let (movie_title, movie_overview) = if let Route::Radarr(_, Some(_)) = app.get_current_route() { @@ -292,7 +266,7 @@ fn draw_confirmation_prompt(f: &mut Frame<'_, B>, app: &mut App, pro .collection_movies .current_selection() .title - .to_string(), + .stationary_style(), app .data .radarr_data @@ -324,20 +298,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 - .add_movie_monitor_list - .current_selection(); + let selected_monitor = app.data.radarr_data.movie_monitor_list.current_selection(); let selected_minimum_availability = app .data .radarr_data - .add_movie_minimum_availability_list + .movie_minimum_availability_list .current_selection(); let selected_quality_profile = app .data .radarr_data - .add_movie_quality_profile_list + .movie_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 a8bc8d3..53d4ca5 100644 --- a/src/ui/radarr_ui/collection_details_ui.rs +++ b/src/ui/radarr_ui/collection_details_ui.rs @@ -119,7 +119,7 @@ pub(super) fn draw_collection_details( TableProps { content: &mut app.data.radarr_data.collection_movies, table_headers: vec![ - "✓", + "✔", "Title", "Year", "Runtime", @@ -147,7 +147,7 @@ pub(super) fn draw_collection_details( .iter() .any(|mov| mov.tmdb_id == movie.tmdb_id) { - "✓" + "✔" } else { "" }; diff --git a/src/ui/radarr_ui/edit_movie_ui.rs b/src/ui/radarr_ui/edit_movie_ui.rs new file mode 100644 index 0000000..cbff227 --- /dev/null +++ b/src/ui/radarr_ui/edit_movie_ui.rs @@ -0,0 +1,169 @@ +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_movie_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::EditMovieSelectMinimumAvailability => { + draw_drop_down_popup( + f, + app, + prompt_area, + draw_edit_confirmation_prompt, + draw_select_minimum_availability_popup, + ); + } + ActiveRadarrBlock::EditMovieSelectQualityProfile => { + draw_drop_down_popup( + f, + app, + prompt_area, + draw_edit_confirmation_prompt, + draw_select_quality_profile_popup, + ); + } + ActiveRadarrBlock::EditMoviePrompt + | ActiveRadarrBlock::EditMovieToggleMonitored + | ActiveRadarrBlock::EditMoviePathInput + | ActiveRadarrBlock::EditMovieTagsInput => draw_edit_confirmation_prompt(f, app, prompt_area), + _ => (), + } + } +} + +fn draw_edit_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 title = format!("Edit - {}", movie_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::EditMovieConfirmPrompt; + + let selected_minimum_availability = app + .data + .radarr_data + .movie_minimum_availability_list + .current_selection(); + let selected_quality_profile = app + .data + .radarr_data + .movie_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(&movie_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::EditMovieToggleMonitored, + ); + + draw_drop_down_menu_button( + f, + chunks[2], + "Minimum Availability", + selected_minimum_availability.to_display_str(), + *selected_block == ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + ); + draw_drop_down_menu_button( + f, + chunks[3], + "Quality Profile", + selected_quality_profile, + *selected_block == ActiveRadarrBlock::EditMovieSelectQualityProfile, + ); + + if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + draw_text_box_with_label( + f, + chunks[4], + "Path", + &app.data.radarr_data.edit_path, + *selected_block == ActiveRadarrBlock::EditMoviePathInput, + active_radarr_block == ActiveRadarrBlock::EditMoviePathInput, + ); + draw_text_box_with_label( + f, + chunks[5], + "Tags", + &app.data.radarr_data.edit_tags, + *selected_block == ActiveRadarrBlock::EditMovieTagsInput, + active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput, + ); + } + + 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/mod.rs b/src/ui/radarr_ui/mod.rs index 1eb8c6a..54f5911 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -6,12 +6,12 @@ use tui::backend::Backend; use tui::layout::{Alignment, Constraint, Rect}; use tui::style::{Color, Style}; use tui::text::Text; -use tui::widgets::{Cell, Paragraph, Row}; +use tui::widgets::{Cell, ListItem, Paragraph, Row}; use tui::Frame; use crate::app::radarr::{ - ActiveRadarrBlock, RadarrData, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, FILTER_BLOCKS, - MOVIE_DETAILS_BLOCKS, SEARCH_BLOCKS, + ActiveRadarrBlock, RadarrData, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, EDIT_MOVIE_BLOCKS, + FILTER_BLOCKS, MOVIE_DETAILS_BLOCKS, SEARCH_BLOCKS, }; use crate::app::App; use crate::logos::RADARR_LOGO; @@ -19,27 +19,29 @@ use crate::models::radarr_models::{DiskSpace, DownloadRecord, Movie}; use crate::models::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_movie_ui::draw_edit_movie_prompt; use crate::ui::radarr_ui::movie_details_ui::draw_movie_info_popup; use crate::ui::utils::{ borderless_block, get_width_from_percentage, horizontal_chunks, layout_block, layout_block_top_border, line_gauge_with_label, line_gauge_with_title, show_cursor, style_bold, - style_default, style_failure, style_primary, style_success, style_warning, title_block, - title_block_centered, vertical_chunks_with_margin, + style_default, style_failure, style_primary, style_success, style_unmonitored, style_warning, + title_block, title_block_centered, vertical_chunks_with_margin, }; use crate::ui::{ - draw_large_popup_over, draw_popup_over, draw_prompt_box, draw_prompt_popup_over, draw_table, - draw_tabs, loading, TableProps, + draw_drop_down_list, draw_large_popup_over, draw_medium_popup_over, draw_popup, draw_popup_over, + draw_prompt_box, draw_prompt_popup_over, draw_table, draw_tabs, loading, TableProps, }; use crate::utils::{convert_runtime, convert_to_gb}; mod add_movie_ui; mod collection_details_ui; +mod edit_movie_ui; mod movie_details_ui; pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let (content_rect, _) = draw_tabs(f, area, "Movies", &app.data.radarr_data.main_tabs); - if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + if let Route::Radarr(active_radarr_block, context_option) = *app.get_current_route() { match active_radarr_block { ActiveRadarrBlock::Movies => draw_library(f, app, content_rect), ActiveRadarrBlock::SearchMovie => { @@ -97,6 +99,20 @@ pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, ar draw_collections, draw_collection_details_popup, ), + _ if EDIT_MOVIE_BLOCKS.contains(&active_radarr_block) => { + if let Some(context) = context_option { + match context { + ActiveRadarrBlock::Movies => { + draw_medium_popup_over(f, app, content_rect, draw_library, draw_edit_movie_prompt) + } + _ if MOVIE_DETAILS_BLOCKS.contains(&context) => { + draw_large_popup_over(f, app, content_rect, draw_library, draw_movie_info_popup); + draw_popup(f, app, draw_edit_movie_prompt, 60, 60); + } + _ => (), + } + } + } ActiveRadarrBlock::DeleteMoviePrompt => { draw_prompt_popup_over(f, app, content_rect, draw_library, draw_delete_movie_prompt) } @@ -160,19 +176,25 @@ fn draw_library(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { table_headers: vec![ "Title", "Year", + "Studio", "Runtime", "Rating", "Language", "Size", "Quality Profile", + "Monitored", + "Tags", ], constraints: vec![ - Constraint::Percentage(25), - Constraint::Percentage(12), - Constraint::Percentage(12), - Constraint::Percentage(12), - Constraint::Percentage(12), - Constraint::Percentage(12), + Constraint::Percentage(27), + Constraint::Percentage(4), + Constraint::Percentage(17), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(10), + Constraint::Percentage(6), Constraint::Percentage(12), ], help: app @@ -182,13 +204,16 @@ fn draw_library(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { .get_active_tab_contextual_help(), }, |movie| { + let monitored = if movie.monitored { "🏷" } else { "" }; let (hours, minutes) = convert_runtime(movie.runtime.as_u64().unwrap()); let file_size: f64 = convert_to_gb(movie.size_on_disk.as_u64().unwrap()); let certification = movie.certification.clone().unwrap_or_else(|| "".to_owned()); + let tags = ""; Row::new(vec![ Cell::from(movie.title.to_owned()), Cell::from(movie.year.to_string()), + Cell::from(movie.studio.to_string()), Cell::from(format!("{}h {}m", hours, minutes)), Cell::from(certification), Cell::from(movie.original_language.name.to_owned()), @@ -199,6 +224,8 @@ fn draw_library(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { .unwrap() .to_owned(), ), + Cell::from(monitored.to_owned()), + Cell::from(tags.to_owned()), ]) .style(determine_row_style(downloads_vec, movie)) }, @@ -601,5 +628,35 @@ fn determine_row_style(downloads_vec: &[DownloadRecord], movie: &Movie) -> Style return style_failure(); } - style_success() + if !movie.monitored { + style_unmonitored() + } else { + style_success() + } +} + +fn draw_select_minimum_availability_popup( + f: &mut Frame<'_, B>, + app: &mut App, + popup_area: Rect, +) { + draw_drop_down_list( + f, + popup_area, + &mut app.data.radarr_data.movie_minimum_availability_list, + |minimum_availability| ListItem::new(minimum_availability.to_display_str().to_owned()), + ); +} + +fn draw_select_quality_profile_popup( + f: &mut Frame<'_, B>, + app: &mut App, + popup_area: Rect, +) { + draw_drop_down_list( + f, + popup_area, + &mut app.data.radarr_data.movie_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 717325b..055dac5 100644 --- a/src/ui/radarr_ui/movie_details_ui.rs +++ b/src/ui/radarr_ui/movie_details_ui.rs @@ -25,8 +25,13 @@ use crate::utils::convert_to_gb; pub(super) fn draw_movie_info_popup(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let (content_area, _) = draw_tabs(f, area, "Movie Info", &app.data.radarr_data.movie_info_tabs); - 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() { + let match_block = if let Some(context) = context_option { + context + } else { + active_radarr_block + }; + match match_block { ActiveRadarrBlock::AutomaticallySearchMoviePrompt => draw_prompt_popup_over( f, app, diff --git a/src/ui/utils.rs b/src/ui/utils.rs index ff83564..43f04eb 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -71,7 +71,7 @@ pub fn layout_button_paragraph(is_selected: bool, label: &str, alignment: Alignm Paragraph::new(Text::from(label)) .block(layout_block()) .alignment(alignment) - .style(style_button_highlight(is_selected)) + .style(style_block_highlight(is_selected)) } pub fn layout_button_paragraph_borderless( @@ -82,7 +82,7 @@ pub fn layout_button_paragraph_borderless( Paragraph::new(Text::from(label)) .block(borderless_block()) .alignment(alignment) - .style(style_button_highlight(is_selected)) + .style(style_block_highlight(is_selected)) } pub fn layout_paragraph_borderless(string: &str) -> Paragraph { @@ -150,6 +150,10 @@ pub fn style_system_function() -> Style { Style::default().fg(Color::Yellow) } +pub fn style_unmonitored() -> Style { + Style::default().fg(Color::Rgb(91, 87, 87)) +} + pub fn style_success() -> Style { Style::default().fg(Color::Green) } @@ -166,7 +170,7 @@ pub fn style_help() -> Style { Style::default().fg(Color::LightBlue) } -pub fn style_button_highlight(is_selected: bool) -> Style { +pub fn style_block_highlight(is_selected: bool) -> Style { if is_selected { style_system_function().add_modifier(Modifier::BOLD) } else { @@ -258,7 +262,7 @@ mod test { horizontal_chunks_with_margin, layout_block, layout_block_bottom_border, layout_block_top_border, layout_block_top_border_with_title, layout_block_with_title, layout_with_constraints, logo_block, spans_info_default, spans_info_primary, - spans_info_with_style, style_bold, style_button_highlight, style_default, style_default_bold, + spans_info_with_style, style_block_highlight, style_bold, style_default, style_default_bold, style_failure, style_help, style_highlight, style_primary, style_secondary, style_success, style_system_function, style_warning, title_block, title_block_centered, title_style, vertical_chunks, vertical_chunks_with_margin, @@ -550,7 +554,7 @@ mod test { .fg(Color::Yellow) .add_modifier(Modifier::BOLD); - assert_eq!(style_button_highlight(true), expected_style); + assert_eq!(style_block_highlight(true), expected_style); } #[test] @@ -559,7 +563,7 @@ mod test { .fg(Color::White) .add_modifier(Modifier::BOLD); - assert_eq!(style_button_highlight(false), expected_style); + assert_eq!(style_block_highlight(false), expected_style); } #[test]