diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 95cb3c6..2d62d1d 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -176,6 +176,7 @@ impl Default for RadarrData { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum ActiveRadarrBlock { + AddMovieAlreadyInLibrary, AddMovieSearchInput, AddMovieSearchResults, AddMoviePrompt, @@ -208,6 +209,31 @@ pub enum ActiveRadarrBlock { ViewMovieOverview, } +pub const ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 7] = [ + ActiveRadarrBlock::AddMovieSearchInput, + ActiveRadarrBlock::AddMovieSearchResults, + ActiveRadarrBlock::AddMoviePrompt, + ActiveRadarrBlock::AddMovieSelectMinimumAvailability, + ActiveRadarrBlock::AddMovieSelectMonitor, + ActiveRadarrBlock::AddMovieSelectQualityProfile, + ActiveRadarrBlock::AddMovieAlreadyInLibrary, +]; +pub const MOVIE_DETAILS_BLOCKS: [ActiveRadarrBlock; 9] = [ + ActiveRadarrBlock::MovieDetails, + ActiveRadarrBlock::MovieHistory, + ActiveRadarrBlock::FileInfo, + ActiveRadarrBlock::Cast, + ActiveRadarrBlock::Crew, + ActiveRadarrBlock::AutomaticallySearchMoviePrompt, + ActiveRadarrBlock::RefreshAndScanPrompt, + ActiveRadarrBlock::ManualSearch, + ActiveRadarrBlock::ManualSearchConfirmPrompt, +]; +pub const COLLECTION_DETAILS_BLOCKS: [ActiveRadarrBlock; 2] = [ + ActiveRadarrBlock::CollectionDetails, + ActiveRadarrBlock::ViewMovieOverview, +]; + impl ActiveRadarrBlock { pub fn next_add_prompt_block(&self) -> Self { match self { diff --git a/src/handlers/radarr_handlers/add_movie_handler.rs b/src/handlers/radarr_handlers/add_movie_handler.rs index 49a34e2..abca057 100644 --- a/src/handlers/radarr_handlers/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/add_movie_handler.rs @@ -184,36 +184,58 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { .items .is_empty() => { - self - .app - .push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); - self + let tmdb_id = self .app .data .radarr_data - .add_movie_monitor_list - .set_items(Vec::from_iter(Monitor::iter())); - self + .add_searched_movies + .current_selection() + .tmdb_id + .clone(); + if self .app .data .radarr_data - .add_movie_minimum_availability_list - .set_items(Vec::from_iter(MinimumAvailability::iter())); - let mut quality_profile_names: Vec = self - .app - .data - .radarr_data - .quality_profile_map - .values() - .cloned() - .collect(); - quality_profile_names.sort(); - self - .app - .data - .radarr_data - .add_movie_quality_profile_list - .set_items(quality_profile_names); + .movies + .items + .iter() + .any(|movie| movie.tmdb_id == tmdb_id) + { + self + .app + .push_navigation_stack(ActiveRadarrBlock::AddMovieAlreadyInLibrary.into()); + } else { + self + .app + .push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); + self + .app + .data + .radarr_data + .add_movie_monitor_list + .set_items(Vec::from_iter(Monitor::iter())); + self + .app + .data + .radarr_data + .add_movie_minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + let mut quality_profile_names: Vec = self + .app + .data + .radarr_data + .quality_profile_map + .values() + .cloned() + .collect(); + quality_profile_names.sort(); + self + .app + .data + .radarr_data + .add_movie_quality_profile_list + .set_items(quality_profile_names); + } } ActiveRadarrBlock::AddMoviePrompt => match self.app.data.radarr_data.selected_block { ActiveRadarrBlock::AddMovieConfirmPrompt => { @@ -261,7 +283,8 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { } ActiveRadarrBlock::AddMovieSelectMonitor | ActiveRadarrBlock::AddMovieSelectMinimumAvailability - | ActiveRadarrBlock::AddMovieSelectQualityProfile => self.app.pop_navigation_stack(), + | ActiveRadarrBlock::AddMovieSelectQualityProfile + | ActiveRadarrBlock::AddMovieAlreadyInLibrary => self.app.pop_navigation_stack(), _ => (), } } @@ -419,6 +442,7 @@ mod tests { use rstest::rstest; use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::models::radarr_models::Movie; use crate::network::radarr_network::RadarrEvent; use super::*; @@ -506,6 +530,33 @@ mod tests { ); } + #[test] + fn test_add_movie_search_results_submit_movie_already_in_library() { + let mut app = App::default(); + app + .data + .radarr_data + .add_searched_movies + .set_items(vec![AddMovieSearchResult::default()]); + app + .data + .radarr_data + .movies + .set_items(vec![Movie::default()]); + + AddMovieHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::AddMovieSearchResults, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::AddMovieAlreadyInLibrary.into() + ); + } + #[test] fn test_add_movie_prompt_prompt_decline() { let mut app = App::default(); @@ -633,6 +684,26 @@ mod tests { assert!(app.should_ignore_quit_key); } + #[test] + fn test_add_movie_already_in_library_esc() { + let mut app = App::default(); + app.data.radarr_data = create_test_radarr_data(); + app.push_navigation_stack(ActiveRadarrBlock::AddMovieSearchResults.into()); + app.push_navigation_stack(ActiveRadarrBlock::AddMovieAlreadyInLibrary.into()); + + AddMovieHandler::with( + &ESC_KEY, + &mut app, + &ActiveRadarrBlock::AddMovieAlreadyInLibrary, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::AddMovieSearchResults.into() + ); + } + #[test] fn test_add_movie_prompt_esc() { let mut app = App::default(); diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 8eff314..afad449 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -1,5 +1,7 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; -use crate::app::radarr::ActiveRadarrBlock; +use crate::app::radarr::{ + ActiveRadarrBlock, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, MOVIE_DETAILS_BLOCKS, +}; use crate::handlers::radarr_handlers::add_movie_handler::AddMovieHandler; use crate::handlers::radarr_handlers::collection_details_handler::CollectionDetailsHandler; use crate::handlers::radarr_handlers::movie_details_handler::MovieDetailsHandler; @@ -22,26 +24,13 @@ pub(super) struct RadarrHandler<'a> { impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { fn handle(&mut self) { match self.active_radarr_block { - ActiveRadarrBlock::MovieDetails - | ActiveRadarrBlock::MovieHistory - | ActiveRadarrBlock::FileInfo - | ActiveRadarrBlock::Cast - | ActiveRadarrBlock::Crew - | ActiveRadarrBlock::AutomaticallySearchMoviePrompt - | ActiveRadarrBlock::RefreshAndScanPrompt - | ActiveRadarrBlock::ManualSearch - | ActiveRadarrBlock::ManualSearchConfirmPrompt => { + _ if MOVIE_DETAILS_BLOCKS.contains(self.active_radarr_block) => { MovieDetailsHandler::with(self.key, self.app, self.active_radarr_block).handle() } - ActiveRadarrBlock::CollectionDetails | ActiveRadarrBlock::ViewMovieOverview => { + _ if COLLECTION_DETAILS_BLOCKS.contains(self.active_radarr_block) => { CollectionDetailsHandler::with(self.key, self.app, self.active_radarr_block).handle() } - ActiveRadarrBlock::AddMovieSearchInput - | ActiveRadarrBlock::AddMovieSearchResults - | ActiveRadarrBlock::AddMoviePrompt - | ActiveRadarrBlock::AddMovieSelectMinimumAvailability - | ActiveRadarrBlock::AddMovieSelectMonitor - | ActiveRadarrBlock::AddMovieSelectQualityProfile => { + _ if ADD_MOVIE_BLOCKS.contains(self.active_radarr_block) => { AddMovieHandler::with(self.key, self.app, self.active_radarr_block).handle() } _ => self.handle_key_event(), @@ -1263,7 +1252,8 @@ mod tests { ActiveRadarrBlock::AddMoviePrompt, ActiveRadarrBlock::AddMovieSelectMonitor, ActiveRadarrBlock::AddMovieSelectMinimumAvailability, - ActiveRadarrBlock::AddMovieSelectQualityProfile + ActiveRadarrBlock::AddMovieSelectQualityProfile, + ActiveRadarrBlock::AddMovieAlreadyInLibrary )] active_radarr_block: ActiveRadarrBlock, ) { diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index e968ad1..141f7b6 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -52,6 +52,8 @@ pub struct Movie { #[derivative(Default(value = "Number::from(0)"))] pub runtime: Number, #[derivative(Default(value = "Number::from(0)"))] + pub tmdb_id: Number, + #[derivative(Default(value = "Number::from(0)"))] pub quality_profile_id: Number, pub certification: Option, pub ratings: RatingsList, diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 0cbba57..4196ae7 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -895,6 +895,7 @@ mod test { const MOVIE_JSON: &str = r#"{ "id": 1, "title": "Test", + "tmdbId": 1234, "originalLanguage": { "name": "English" }, @@ -1462,6 +1463,7 @@ mod test { "monitored": true, "hasFile": false, "runtime": 120, + "tmdbId": 1234, "qualityProfileId": 2222, "ratings": {} }); @@ -2161,6 +2163,7 @@ mod test { monitored: true, has_file: true, runtime: Number::from(120), + tmdb_id: Number::from(1234), quality_profile_id: Number::from(2222), certification: Some("R".to_owned()), ratings: ratings_list(), diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 6a6ef49..f0c0833 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,5 +1,6 @@ use tui::backend::Backend; use tui::layout::{Alignment, Constraint, Rect}; +use tui::style::Modifier; use tui::text::{Span, Spans, Text}; use tui::widgets::Paragraph; use tui::widgets::Row; @@ -291,6 +292,30 @@ pub fn loading(f: &mut Frame<'_, B>, block: Block<'_>, area: Rect, i } } +pub fn draw_error_popup_over( + f: &mut Frame<'_, B>, + app: &mut App, + area: Rect, + message: &str, + background_fn: fn(&mut Frame<'_, B>, &mut App, Rect), +) { + background_fn(f, app, area); + draw_error_popup(f, message); +} + +pub fn draw_error_popup(f: &mut Frame<'_, B>, message: &str) { + let prompt_area = centered_rect(25, 8, f.size()); + f.render_widget(Clear, prompt_area); + + let error_message = Paragraph::new(Text::from(message)) + .block(title_block_centered("Error").style(style_failure())) + .style(style_failure().add_modifier(Modifier::BOLD)) + .wrap(Wrap { trim: false }) + .alignment(Alignment::Center); + + f.render_widget(error_message, prompt_area); +} + pub fn draw_prompt_box( f: &mut Frame<'_, B>, prompt_area: Rect, diff --git a/src/ui/radarr_ui/add_movie_ui.rs b/src/ui/radarr_ui/add_movie_ui.rs index c097ef3..c4a1310 100644 --- a/src/ui/radarr_ui/add_movie_ui.rs +++ b/src/ui/radarr_ui/add_movie_ui.rs @@ -8,13 +8,13 @@ use crate::app::radarr::ActiveRadarrBlock; use crate::models::radarr_models::AddMovieSearchResult; use crate::models::Route; use crate::ui::utils::{ - borderless_block, centered_rect, get_width_with_margin, horizontal_chunks, layout_block, - layout_error_paragraph, layout_paragraph_borderless, show_cursor, style_default, style_help, - style_primary, title_block_centered, vertical_chunks_with_margin, + borderless_block, get_width_with_margin, horizontal_chunks, layout_block, + layout_paragraph_borderless, show_cursor, style_default, 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_medium_popup_over, draw_table, TableProps, + draw_error_popup, draw_error_popup_over, draw_medium_popup_over, draw_table, TableProps, }; use crate::utils::convert_runtime; use crate::App; @@ -35,6 +35,13 @@ pub(super) fn draw_add_movie_search_popup( | ActiveRadarrBlock::AddMovieSelectQualityProfile => { draw_medium_popup_over(f, app, area, draw_add_movie_search, draw_confirmation_popup); } + ActiveRadarrBlock::AddMovieAlreadyInLibrary => draw_error_popup_over( + f, + app, + area, + "This film is already in your library", + draw_add_movie_search, + ), _ => (), } } @@ -83,7 +90,8 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: | ActiveRadarrBlock::AddMoviePrompt | ActiveRadarrBlock::AddMovieSelectMonitor | ActiveRadarrBlock::AddMovieSelectMinimumAvailability - | ActiveRadarrBlock::AddMovieSelectQualityProfile => { + | ActiveRadarrBlock::AddMovieSelectQualityProfile + | ActiveRadarrBlock::AddMovieAlreadyInLibrary => { let mut help_text = Text::from(" details | edit search"); help_text.patch_style(style_help()); let help_paragraph = Paragraph::new(help_text) @@ -96,10 +104,7 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: && !app.is_routing { f.render_widget(layout_block(), chunks[1]); - f.render_widget( - layout_error_paragraph("No movies found matching your query!"), - centered_rect(30, 10, chunks[1]), - ); + draw_error_popup(f, "No movies found matching your query!"); } else { draw_table( f, diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index acdfca5..a9c12de 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -9,7 +9,9 @@ use tui::text::Text; use tui::widgets::{Cell, Paragraph, Row}; use tui::Frame; -use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; +use crate::app::radarr::{ + ActiveRadarrBlock, RadarrData, ADD_MOVIE_BLOCKS, COLLECTION_DETAILS_BLOCKS, MOVIE_DETAILS_BLOCKS, +}; use crate::app::App; use crate::logos::RADARR_LOGO; use crate::models::radarr_models::{DiskSpace, DownloadRecord, Movie}; @@ -55,38 +57,23 @@ pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, ar } ActiveRadarrBlock::Downloads => draw_downloads(f, app, content_rect), ActiveRadarrBlock::Collections => draw_collections(f, app, content_rect), - ActiveRadarrBlock::MovieDetails - | ActiveRadarrBlock::MovieHistory - | ActiveRadarrBlock::FileInfo - | ActiveRadarrBlock::Cast - | ActiveRadarrBlock::Crew - | ActiveRadarrBlock::AutomaticallySearchMoviePrompt - | ActiveRadarrBlock::RefreshAndScanPrompt - | ActiveRadarrBlock::ManualSearch - | ActiveRadarrBlock::ManualSearchConfirmPrompt => { + _ if MOVIE_DETAILS_BLOCKS.contains(&active_radarr_block) => { draw_large_popup_over(f, app, content_rect, draw_library, draw_movie_info_popup) } - ActiveRadarrBlock::AddMovieSearchInput - | ActiveRadarrBlock::AddMovieSearchResults - | ActiveRadarrBlock::AddMoviePrompt - | ActiveRadarrBlock::AddMovieSelectMonitor - | ActiveRadarrBlock::AddMovieSelectMinimumAvailability - | ActiveRadarrBlock::AddMovieSelectQualityProfile => draw_large_popup_over( + _ if ADD_MOVIE_BLOCKS.contains(&active_radarr_block) => draw_large_popup_over( f, app, content_rect, draw_library, draw_add_movie_search_popup, ), - ActiveRadarrBlock::CollectionDetails | ActiveRadarrBlock::ViewMovieOverview => { - draw_large_popup_over( - f, - app, - content_rect, - draw_collections, - draw_collection_details_popup, - ) - } + _ if COLLECTION_DETAILS_BLOCKS.contains(&active_radarr_block) => draw_large_popup_over( + f, + app, + content_rect, + draw_collections, + draw_collection_details_popup, + ), ActiveRadarrBlock::DeleteMoviePrompt => { draw_prompt_popup_over(f, app, content_rect, draw_library, draw_delete_movie_prompt) } diff --git a/src/ui/utils.rs b/src/ui/utils.rs index 5d73992..ac1c33a 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -93,14 +93,6 @@ pub fn layout_paragraph_borderless(string: &str) -> Paragraph { .alignment(Alignment::Center) } -pub fn layout_error_paragraph(string: &str) -> Paragraph { - Paragraph::new(Text::from(string)) - .block(layout_block_with_title(title_style("Error"))) - .style(style_failure().add_modifier(Modifier::BOLD)) - .wrap(Wrap { trim: false }) - .alignment(Alignment::Center) -} - pub fn borderless_block<'a>() -> Block<'a> { Block::default() }