diff --git a/Cargo.toml b/Cargo.toml index df58fbd..7abe6db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,4 @@ serde = { version = "1.0", features = ["derive"] } strum = { version = "0.24.1", features = ["derive"] } tokio = { version = "1.24.1", features = ["full"] } tui = "0.19.0" +urlencoding = "2.1.2" diff --git a/src/app/key_binding.rs b/src/app/key_binding.rs index 5be0e26..774e1c9 100644 --- a/src/app/key_binding.rs +++ b/src/app/key_binding.rs @@ -9,6 +9,7 @@ macro_rules! generate_keybindings { } generate_keybindings! { + add, up, down, left, @@ -30,6 +31,10 @@ pub struct KeyBinding { } pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { + add: KeyBinding { + key: Key::Char('a'), + desc: "Add", + }, up: KeyBinding { key: Key::Up, desc: "Scroll up", diff --git a/src/app/mod.rs b/src/app/mod.rs index c22a57e..18c50bb 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -30,6 +30,7 @@ pub struct App { pub is_routing: bool, pub is_loading: bool, pub should_refresh: bool, + pub should_ignore_quit_key: bool, pub config: AppConfig, pub data: Data, } @@ -119,7 +120,8 @@ impl Default for App { TabRoute { title: "Radarr".to_owned(), route: ActiveRadarrBlock::Movies.into(), - help: " change servarr | help | quit ".to_owned(), + help: "<↑↓> scroll | ←→ change tab | change servarr | help | quit " + .to_owned(), }, TabRoute { title: "Sonarr".to_owned(), @@ -136,6 +138,7 @@ impl Default for App { is_loading: false, is_routing: false, should_refresh: false, + should_ignore_quit_key: false, config: AppConfig::default(), data: Data::default(), } diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 8fc99ae..d868d2c 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -5,10 +5,11 @@ use chrono::{DateTime, Utc}; use strum::EnumIter; use crate::app::{App, Route}; -use crate::models::{ScrollableText, StatefulMatrix, StatefulTable, TabRoute, TabState}; use crate::models::radarr_models::{ - Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord, Movie, MovieHistoryItem, + AddMovieSearchResult, Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord, Movie, + MovieHistoryItem, }; +use crate::models::{ScrollableText, StatefulTable, TabRoute, TabState}; use crate::network::radarr_network::RadarrEvent; pub struct RadarrData { @@ -17,6 +18,7 @@ pub struct RadarrData { pub start_time: DateTime, pub movies: StatefulTable, pub filtered_movies: StatefulTable, + pub add_searched_movies: StatefulTable, pub downloads: StatefulTable, pub quality_profile_map: HashMap, pub movie_details: ScrollableText, @@ -29,7 +31,6 @@ pub struct RadarrData { pub collections: StatefulTable, pub filtered_collections: StatefulTable, pub collection_movies: StatefulTable, - pub calendar: StatefulMatrix<> pub main_tabs: TabState, pub movie_info_tabs: TabState, pub search: String, @@ -49,6 +50,7 @@ impl RadarrData { self.filter = String::default(); self.filtered_movies = StatefulTable::default(); self.filtered_collections = StatefulTable::default(); + self.add_searched_movies = StatefulTable::default(); } pub fn reset_movie_info_tabs(&mut self) { @@ -74,6 +76,7 @@ impl Default for RadarrData { version: String::default(), start_time: DateTime::default(), movies: StatefulTable::default(), + add_searched_movies: StatefulTable::default(), filtered_movies: StatefulTable::default(), downloads: StatefulTable::default(), quality_profile_map: HashMap::default(), @@ -95,25 +98,20 @@ impl Default for RadarrData { TabRoute { title: "Library".to_owned(), route: ActiveRadarrBlock::Movies.into(), - help: "<↑↓> scroll | search | filter | details | cancel filter | delete | ←→ change tab " + help: " add | search | filter | details | cancel filter | delete " .to_owned(), }, TabRoute { title: "Downloads".to_owned(), route: ActiveRadarrBlock::Downloads.into(), - help: "<↑↓> scroll | ←→ change tab ".to_owned(), + help: String::default(), }, TabRoute { title: "Collections".to_owned(), route: ActiveRadarrBlock::Collections.into(), - help: "<↑↓> scroll | search | filter | details | cancel filter | ←→ change tab " + help: " search | filter | details | cancel filter " .to_owned(), }, - TabRoute { - title: "Calendar".to_owned(), - route: ActiveRadarrBlock::Calendar.into(), - help: "<↑↓> scroll | details | ←→ change tab ".to_owned() - } ]), movie_info_tabs: TabState::new(vec![ TabRoute { @@ -148,7 +146,9 @@ impl Default for RadarrData { #[derive(Clone, PartialEq, Eq, Debug, EnumIter)] pub enum ActiveRadarrBlock { - AddMovie, + AddMovieSearchInput, + AddMovieSearchResults, + AddMoviePrompt, Calendar, Collections, CollectionDetails, @@ -209,6 +209,12 @@ impl App { .await; } } + ActiveRadarrBlock::AddMovieSearchResults => { + self.is_loading = true; + self + .dispatch_network_event(RadarrEvent::SearchNewMovie.into()) + .await; + } ActiveRadarrBlock::MovieDetails | ActiveRadarrBlock::FileInfo => { self.is_loading = true; self diff --git a/src/handlers/radarr_handlers/add_movie_handler.rs b/src/handlers/radarr_handlers/add_movie_handler.rs new file mode 100644 index 0000000..a1706be --- /dev/null +++ b/src/handlers/radarr_handlers/add_movie_handler.rs @@ -0,0 +1,139 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::radarr::ActiveRadarrBlock; +use crate::handlers::KeyEventHandler; +use crate::models::{Scrollable, StatefulTable}; +use crate::{App, Key}; + +pub(super) struct AddMovieHandler<'a> { + key: &'a Key, + app: &'a mut App, + active_radarr_block: &'a ActiveRadarrBlock, +} + +impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { + fn with( + key: &'a Key, + app: &'a mut App, + active_block: &'a ActiveRadarrBlock, + ) -> AddMovieHandler<'a> { + AddMovieHandler { + key, + app, + active_radarr_block: active_block, + } + } + + fn get_key(&self) -> &Key { + self.key + } + + fn handle_scroll_up(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::AddMovieSearchResults => { + self.app.data.radarr_data.add_searched_movies.scroll_up() + } + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::AddMovieSearchResults => { + self.app.data.radarr_data.add_searched_movies.scroll_down() + } + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::AddMovieSearchResults => self + .app + .data + .radarr_data + .add_searched_movies + .scroll_to_top(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::AddMovieSearchResults => self + .app + .data + .radarr_data + .add_searched_movies + .scroll_to_bottom(), + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if *self.active_radarr_block == ActiveRadarrBlock::AddMoviePrompt { + match self.key { + _ if *self.key == DEFAULT_KEYBINDINGS.left.key + || *self.key == DEFAULT_KEYBINDINGS.right.key => + { + self.app.data.radarr_data.prompt_confirm = !self.app.data.radarr_data.prompt_confirm; + } + _ => (), + } + } + } + + fn handle_submit(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::AddMovieSearchInput => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::AddMovieSearchResults.into()); + self.app.should_ignore_quit_key = false; + } + ActiveRadarrBlock::AddMovieSearchResults => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::AddMoviePrompt.into()); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::AddMovieSearchInput => { + self.app.pop_navigation_stack(); + self.app.data.radarr_data.reset_search(); + self.app.should_ignore_quit_key = false; + } + ActiveRadarrBlock::AddMovieSearchResults => { + self.app.pop_navigation_stack(); + self.app.data.radarr_data.add_searched_movies = StatefulTable::default(); + self.app.should_ignore_quit_key = true; + } + ActiveRadarrBlock::AddMoviePrompt => { + self.app.pop_navigation_stack(); + self.app.data.radarr_data.prompt_confirm = false; + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_radarr_block { + ActiveRadarrBlock::AddMovieSearchInput => match self.key { + _ if *key == DEFAULT_KEYBINDINGS.backspace.key => { + self.app.data.radarr_data.search.pop(); + } + Key::Char(character) => { + self.app.data.radarr_data.search.push(*character); + } + _ => (), + }, + _ => (), + } + } +} diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 76fd56f..43d38ca 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -1,5 +1,6 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::ActiveRadarrBlock; +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; use crate::handlers::{handle_clear_errors, KeyEventHandler}; @@ -7,6 +8,7 @@ use crate::models::Scrollable; use crate::utils::strip_non_alphanumeric_characters; use crate::{App, Key}; +mod add_movie_handler; mod collection_details_handler; mod movie_details_handler; @@ -29,6 +31,11 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { ActiveRadarrBlock::CollectionDetails | ActiveRadarrBlock::ViewMovieOverview => { CollectionDetailsHandler::with(self.key, self.app, self.active_radarr_block).handle() } + ActiveRadarrBlock::AddMovieSearchInput + | ActiveRadarrBlock::AddMovieSearchResults + | ActiveRadarrBlock::AddMoviePrompt => { + AddMovieHandler::with(self.key, self.app, self.active_radarr_block).handle() + } _ => self.handle_key_event(), } } @@ -300,6 +307,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { | ActiveRadarrBlock::FilterCollections => { self.app.pop_navigation_stack(); self.app.data.radarr_data.reset_search(); + self.app.should_ignore_quit_key = false; } ActiveRadarrBlock::DeleteMoviePrompt => { self.app.pop_navigation_stack(); @@ -321,12 +329,20 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { .app .push_navigation_stack(ActiveRadarrBlock::SearchMovie.into()); self.app.data.radarr_data.is_searching = true; + self.app.should_ignore_quit_key = true; } _ if *key == DEFAULT_KEYBINDINGS.filter.key => { self .app .push_navigation_stack(ActiveRadarrBlock::FilterMovies.into()); self.app.data.radarr_data.is_searching = true; + self.app.should_ignore_quit_key = true; + } + _ if *key == DEFAULT_KEYBINDINGS.add.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::AddMovieSearchInput.into()); + self.app.should_ignore_quit_key = true; } _ => (), }, @@ -336,12 +352,14 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { .app .push_navigation_stack(ActiveRadarrBlock::SearchCollection.into()); self.app.data.radarr_data.is_searching = true; + self.app.should_ignore_quit_key = true; } _ if *key == DEFAULT_KEYBINDINGS.filter.key => { self .app .push_navigation_stack(ActiveRadarrBlock::FilterCollections.into()); self.app.data.radarr_data.is_searching = true; + self.app.should_ignore_quit_key = true; } _ => (), }, @@ -386,6 +404,7 @@ impl RadarrHandler<'_> { }); self.app.data.radarr_data.is_searching = false; + self.app.should_ignore_quit_key = false; if collection_index.is_some() { self.app.pop_navigation_stack(); @@ -415,6 +434,7 @@ impl RadarrHandler<'_> { .collect(); self.app.data.radarr_data.is_searching = false; + self.app.should_ignore_quit_key = false; if !filter_matches.is_empty() { self.app.pop_navigation_stack(); diff --git a/src/main.rs b/src/main.rs index 78c8eba..02e3882 100644 --- a/src/main.rs +++ b/src/main.rs @@ -83,7 +83,7 @@ async fn start_ui(app: &Arc>) -> Result<()> { match input_events.next()? { InputEvent::KeyEvent(key) => { - if key == Key::Char('q') { + if key == Key::Char('q') && !app.should_ignore_quit_key { break; } diff --git a/src/models/mod.rs b/src/models/mod.rs index 7f32675..e520508 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -113,61 +113,6 @@ impl Scrollable for StatefulTable { } } -#[derive(Default)] -pub struct StatefulMatrix { - pub selection: (usize, usize), - pub items: Vec>, -} - -impl Scrollable for StatefulMatrix { - fn scroll_down(&mut self) { - if self.selection.0 >= self.items.len() - 1 { - self.selection.0 = 0; - } else { - self.selection.0 += 1; - } - } - - fn scroll_up(&mut self) { - if self.selection.0 == 0 { - self.selection.0 = self.items.len() - 1; - } else { - self.selection.0 -= 1; - } - } - - fn scroll_to_top(&mut self) { - self.selection.0 = 0; - } - - fn scroll_to_bottom(&mut self) { - self.selection.0 = self.items.len() - 1; - } -} - -impl StatefulMatrix { - pub fn current_selection(&self) -> &T { - let (x, y) = self.selection; - &self.items[x][y] - } - - pub fn scroll_left(&mut self) { - if self.selection.1 == 0 { - self.selection.1 = self.items[0].len() - 1; - } else { - self.selection.1 -= 1; - } - } - - pub fn scroll_right(&mut self) { - if self.selection.1 >= self.items[0].len() - 1 { - self.selection.1 = 0; - } else { - self.selection.1 += 1; - } - } -} - #[derive(Default)] pub struct ScrollableText { pub items: Vec, diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 61c56fc..28f4df3 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; use derivative::Derivative; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::Number; use crate::models::HorizontallyScrollableText; @@ -199,3 +199,39 @@ pub struct Credit { #[serde(rename(deserialize = "type"))] pub credit_type: CreditType, } + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AddMovieBody { + pub tmdb_id: Number, + pub title: String, + pub root_folder_path: String, + pub quality_profile_id: Number, + pub minimum_availability: String, + pub monitored: bool, + pub add_options: AddOptions, +} + +#[derive(Serialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AddOptions { + pub search_for_movie: bool, +} + +#[derive(Derivative, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derivative(Default)] +#[serde(rename_all = "camelCase")] +pub struct AddMovieSearchResult { + #[derivative(Default(value = "Number::from(0)"))] + pub tmdb_id: Number, + pub title: String, + pub original_language: Language, + pub status: String, + pub overview: String, + pub genres: Vec, + #[derivative(Default(value = "Number::from(0)"))] + pub year: Number, + #[derivative(Default(value = "Number::from(0)"))] + pub runtime: Number, + pub ratings: RatingsList, +} diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 83ffa23..10a2258 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -4,11 +4,12 @@ use log::{debug, error}; use reqwest::{RequestBuilder, StatusCode}; use serde::de::DeserializeOwned; use tokio::sync::MutexGuard; +use urlencoding::encode; use crate::app::{App, RadarrConfig}; use crate::models::radarr_models::{ - Collection, Credit, CreditType, DiskSpace, DownloadsResponse, Movie, MovieHistoryItem, - QualityProfile, SystemStatus, + AddMovieSearchResult, Collection, Credit, CreditType, DiskSpace, DownloadsResponse, Movie, + MovieHistoryItem, QualityProfile, SystemStatus, }; use crate::models::ScrollableText; use crate::network::utils::get_movie_status; @@ -27,6 +28,7 @@ pub enum RadarrEvent { GetOverview, GetQualityProfiles, GetStatus, + SearchNewMovie, HealthCheck, } @@ -48,6 +50,7 @@ impl RadarrEvent { RadarrEvent::GetCollections => "/collection", RadarrEvent::GetDownloads => "/queue", RadarrEvent::GetMovies | RadarrEvent::GetMovieDetails | RadarrEvent::DeleteMovie => "/movie", + RadarrEvent::SearchNewMovie => "/movie/lookup", RadarrEvent::GetMovieCredits => "/credit", RadarrEvent::GetMovieHistory => "/history/movie", RadarrEvent::GetOverview => "/diskspace", @@ -122,6 +125,11 @@ impl<'a> Network<'a> { .get_quality_profiles(RadarrEvent::GetQualityProfiles.resource().to_owned()) .await } + RadarrEvent::SearchNewMovie => { + self + .search_movie(RadarrEvent::SearchNewMovie.resource().to_owned()) + .await + } } } @@ -142,13 +150,13 @@ impl<'a> Network<'a> { } async fn get_diskspace(&self, resource: String) { - type RequestType = Vec; + type ResponseType = Vec; self - .handle_request::( + .handle_request::( RequestProps { resource, method: RequestMethod::GET, - body: None::, + body: None::, }, |disk_space_vec, mut app| { app.data.radarr_data.disk_space_vec = disk_space_vec; @@ -174,19 +182,44 @@ impl<'a> Network<'a> { } async fn get_movies(&self, resource: String) { - type RequestType = Vec; + type ResponseType = Vec; self - .handle_request::( + .handle_request::( RequestProps { resource, method: RequestMethod::GET, - body: None::, + body: None::, }, |movie_vec, mut app| app.data.radarr_data.movies.set_items(movie_vec), ) .await; } + async fn search_movie(&self, resource: String) { + type ResponseType = Vec; + let search_string = self.app.lock().await.data.radarr_data.search.clone(); + debug!( + "Searching for movie: {:?}", + format!("{}?term={}", resource, encode(search_string.as_str())) + ); + self + .handle_request::( + RequestProps { + resource: format!("{}?term={}", resource, encode(&search_string)), + method: RequestMethod::GET, + body: None::, + }, + |movie_vec, mut app| { + app + .data + .radarr_data + .add_searched_movies + .set_items(movie_vec) + }, + ) + .await; + } + async fn get_movie_details(&self, resource: String) { let movie_id = self.extract_movie_id().await; self @@ -338,13 +371,13 @@ impl<'a> Network<'a> { } async fn get_movie_history(&self, resource: String) { - type RequestType = Vec; + type ResponseType = Vec; self - .handle_request::( + .handle_request::( RequestProps { resource: self.append_movie_id_param(&resource).await, method: RequestMethod::GET, - body: None::, + body: None::, }, |movie_history_vec, mut app| { let mut reversed_movie_history_vec = movie_history_vec.to_vec(); @@ -360,13 +393,13 @@ impl<'a> Network<'a> { } async fn get_collections(&self, resource: String) { - type RequestType = Vec; + type ResponseType = Vec; self - .handle_request::( + .handle_request::( RequestProps { resource, method: RequestMethod::GET, - body: None::, + body: None::, }, |collections_vec, mut app| { app.data.radarr_data.collections.set_items(collections_vec); @@ -395,13 +428,13 @@ impl<'a> Network<'a> { } async fn get_quality_profiles(&self, resource: String) { - type RequestType = Vec; + type ResponseType = Vec; self - .handle_request::( + .handle_request::( RequestProps { resource, method: RequestMethod::GET, - body: None::, + body: None::, }, |quality_profiles, mut app| { app.data.radarr_data.quality_profile_map = quality_profiles @@ -414,13 +447,13 @@ impl<'a> Network<'a> { } async fn get_credits(&self, resource: String) { - type RequestType = Vec; + type ResponseType = Vec; self - .handle_request::( + .handle_request::( RequestProps { resource: self.append_movie_id_param(&resource).await, method: RequestMethod::GET, - body: None::, + body: None::, }, |credit_vec, mut app| { let cast_vec: Vec = credit_vec diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 61338ed..edc879b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -16,7 +16,7 @@ use crate::ui::utils::{ borderless_block, centered_rect, horizontal_chunks, horizontal_chunks_with_margin, layout_block, layout_block_top_border, logo_block, style_default_bold, style_failure, style_help, style_highlight, style_primary, style_secondary, style_system_function, title_block, - vertical_chunks_with_margin, + title_block_centered, vertical_chunks_with_margin, }; mod radarr_ui; @@ -259,10 +259,7 @@ pub fn draw_prompt_box( prompt: &str, yes_no_value: &bool, ) { - f.render_widget( - title_block(title).title_alignment(Alignment::Center), - prompt_area, - ); + f.render_widget(title_block_centered(title), prompt_area); let chunks = vertical_chunks_with_margin( vec![ diff --git a/src/ui/radarr_ui/add_movie_ui.rs b/src/ui/radarr_ui/add_movie_ui.rs new file mode 100644 index 0000000..188d515 --- /dev/null +++ b/src/ui/radarr_ui/add_movie_ui.rs @@ -0,0 +1,177 @@ +use tui::backend::Backend; +use tui::layout::{Alignment, Constraint, Rect}; +use tui::text::Text; +use tui::widgets::{Cell, Paragraph, Row}; +use tui::Frame; + +use crate::app::radarr::ActiveRadarrBlock; +use crate::models::Route; +use crate::ui::utils::{ + borderless_block, layout_block, show_cursor, style_default, style_help, style_primary, + title_block_centered, vertical_chunks_with_margin, +}; +use crate::ui::{draw_medium_popup_over, draw_prompt_box, draw_table, TableProps}; +use crate::utils::convert_runtime; +use crate::App; + +pub(super) fn draw_add_movie_search_popup( + f: &mut Frame<'_, B>, + app: &mut App, + area: Rect, +) { + if let Route::Radarr(active_radarr_block) = app.get_current_route().clone() { + match active_radarr_block { + ActiveRadarrBlock::AddMovieSearchInput | ActiveRadarrBlock::AddMovieSearchResults => { + draw_add_movie_search(f, app, area); + } + ActiveRadarrBlock::AddMoviePrompt => { + draw_medium_popup_over( + f, + app, + area, + draw_add_movie_search, + draw_add_movie_confirmation_prompt, + ); + } + _ => (), + } + } +} + +fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let chunks = vertical_chunks_with_margin( + vec![ + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(3), + ], + area, + 1, + ); + 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().clone() { + match active_radarr_block { + ActiveRadarrBlock::AddMovieSearchInput => { + show_cursor(f, chunks[0], block_content); + f.render_widget(layout_block(), chunks[1]); + + let mut help_text = Text::from(" close"); + help_text.patch_style(style_help()); + let help_paragraph = Paragraph::new(help_text) + .block(borderless_block()) + .alignment(Alignment::Center); + f.render_widget(help_paragraph, chunks[2]); + } + ActiveRadarrBlock::AddMovieSearchResults | ActiveRadarrBlock::AddMoviePrompt => { + let mut help_text = Text::from(" edit search"); + help_text.patch_style(style_help()); + let help_paragraph = Paragraph::new(help_text) + .block(borderless_block()) + .alignment(Alignment::Center); + f.render_widget(help_paragraph, chunks[2]); + + draw_table( + f, + chunks[1], + layout_block(), + TableProps { + content: &mut app.data.radarr_data.add_searched_movies, + table_headers: vec![ + "Title", + "Year", + "Runtime", + "IMDB Rating", + "Rotten Tomatoes Rating", + "Genres", + ], + constraints: vec![ + Constraint::Percentage(20), + Constraint::Percentage(8), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(18), + Constraint::Percentage(30), + ], + }, + |movie| { + let (hours, minutes) = convert_runtime(movie.runtime.as_u64().unwrap()); + let imdb_rating = movie + .ratings + .imdb + .clone() + .unwrap_or_default() + .value + .as_f64() + .unwrap(); + let rotten_tomatoes_rating = movie + .ratings + .rotten_tomatoes + .clone() + .unwrap_or_default() + .value + .as_u64() + .unwrap(); + let imdb_rating = if imdb_rating == 0.0 { + String::default() + } else { + format!("{:.1}", imdb_rating) + }; + let rotten_tomatoes_rating = if rotten_tomatoes_rating == 0 { + String::default() + } else { + format!("{}%", rotten_tomatoes_rating) + }; + + Row::new(vec![ + Cell::from(movie.title.to_owned()), + Cell::from(movie.year.as_u64().unwrap().to_string()), + Cell::from(format!("{}h {}m", hours, minutes)), + Cell::from(imdb_rating), + Cell::from(rotten_tomatoes_rating), + Cell::from(movie.genres.join(", ")), + ]) + .style(style_primary()) + }, + app.is_loading, + ); + } + _ => (), + } + } + + f.render_widget(search_paragraph, chunks[0]); +} + +fn draw_add_movie_confirmation_prompt( + f: &mut Frame<'_, B>, + app: &mut App, + prompt_area: Rect, +) { + draw_prompt_box( + f, + prompt_area, + " Confirm Add Movie? ", + format!( + "{}:\n\n{}", + app + .data + .radarr_data + .add_searched_movies + .current_selection() + .title, + app + .data + .radarr_data + .add_searched_movies + .current_selection() + .overview + ) + .as_str(), + &app.data.radarr_data.prompt_confirm, + ); +} diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index 821cc3f..7cbc6c6 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -14,12 +14,13 @@ use crate::app::App; use crate::logos::RADARR_LOGO; 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::movie_details_ui::draw_movie_info; use crate::ui::utils::{ borderless_block, 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, + style_failure, style_primary, style_success, style_warning, title_block, title_block_centered, vertical_chunks_with_margin, }; use crate::ui::{ @@ -28,6 +29,7 @@ use crate::ui::{ }; use crate::utils::{convert_runtime, convert_to_gb}; +mod add_movie_ui; mod collection_details_ui; mod movie_details_ui; @@ -60,6 +62,15 @@ pub(super) fn draw_radarr_ui(f: &mut Frame<'_, B>, app: &mut App, ar | ActiveRadarrBlock::Crew => { draw_large_popup_over(f, app, content_rect, draw_library, draw_movie_info) } + ActiveRadarrBlock::AddMovieSearchInput + | ActiveRadarrBlock::AddMovieSearchResults + | ActiveRadarrBlock::AddMoviePrompt => draw_large_popup_over( + f, + app, + content_rect, + draw_library, + draw_add_movie_search_popup, + ), ActiveRadarrBlock::CollectionDetails | ActiveRadarrBlock::ViewMovieOverview => { draw_large_popup_over( f, @@ -200,7 +211,7 @@ fn draw_search_box(f: &mut Frame<'_, B>, app: &mut App, area: Rect) let input = Paragraph::new(block_content) .style(style_default()) - .block(title_block(block_title)); + .block(title_block_centered(block_title)); show_cursor(f, chunks[0], block_content); f.render_widget(input, chunks[0]); diff --git a/src/ui/utils.rs b/src/ui/utils.rs index a7d5655..e37f50f 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -1,5 +1,5 @@ use tui::backend::Backend; -use tui::layout::{Constraint, Direction, Layout, Rect}; +use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use tui::style::{Color, Modifier, Style}; use tui::text::{Span, Spans}; use tui::widgets::{Block, Borders, LineGauge}; @@ -152,6 +152,10 @@ pub fn title_block(title: &str) -> Block<'_> { layout_block_with_title(title_style(title)) } +pub fn title_block_centered(title: &str) -> Block<'_> { + title_block(title).title_alignment(Alignment::Center) +} + pub fn logo_block<'a>() -> Block<'a> { layout_block().title(Span::styled( " Managarr - A Servarr management TUI ",