diff --git a/src/handlers/radarr_handlers/add_movie_handler.rs b/src/handlers/radarr_handlers/add_movie_handler.rs index cae32e0..71eb1f9 100644 --- a/src/handlers/radarr_handlers/add_movie_handler.rs +++ b/src/handlers/radarr_handlers/add_movie_handler.rs @@ -189,14 +189,15 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> { .radarr_data .add_movie_minimum_availability_list .set_items(MinimumAvailability::vec()); - let quality_profile_names = self + let mut quality_profile_names: Vec = self .app .data .radarr_data .quality_profile_map - .iter() - .map(|(_, value)| value.clone()) + .values() + .cloned() .collect(); + quality_profile_names.sort(); self .app .data diff --git a/src/models/mod.rs b/src/models/mod.rs index 34b1922..5038bd8 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -273,6 +273,14 @@ impl HorizontallyScrollableText { pub fn reset_offset(&self) { *self.offset.borrow_mut() = 0; } + + pub fn scroll_or_reset(&self, width: usize, is_current_selection: bool) { + if is_current_selection && self.text.len() > width { + self.scroll_text(); + } else { + self.reset_offset(); + } + } } #[derive(Clone)] diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index c4c6a0c..35dfb97 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -235,7 +235,7 @@ pub struct AddOptions { pub struct AddMovieSearchResult { #[derivative(Default(value = "Number::from(0)"))] pub tmdb_id: Number, - pub title: String, + pub title: HorizontallyScrollableText, pub original_language: Language, pub status: String, pub overview: String, @@ -249,11 +249,11 @@ pub struct AddMovieSearchResult { #[derive(Default, PartialEq, Eq, Clone, Debug)] pub enum MinimumAvailability { - Tba, + #[default] Announced, InCinemas, - #[default] Released, + Tba, } impl Display for MinimumAvailability { @@ -271,19 +271,19 @@ impl Display for MinimumAvailability { impl MinimumAvailability { pub fn vec() -> Vec { vec![ - MinimumAvailability::Tba, MinimumAvailability::Announced, MinimumAvailability::InCinemas, MinimumAvailability::Released, + MinimumAvailability::Tba, ] } pub fn to_display_str(&self) -> &str { match self { - MinimumAvailability::Tba => "TBA", MinimumAvailability::Announced => "Announced", MinimumAvailability::InCinemas => "In Cinemas", MinimumAvailability::Released => "Released", + MinimumAvailability::Tba => "TBA", } } } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 1fa967d..971ad96 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -540,7 +540,7 @@ impl<'a> Network<'a> { AddMovieBody { tmdb_id: tmdb_id.as_u64().unwrap(), - title, + title: title.to_string(), root_folder_path: path.to_owned(), minimum_availability, monitored: true, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index a93ac3c..f3f360f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,22 +1,23 @@ use tui::backend::Backend; use tui::layout::{Alignment, Constraint, Rect}; -use tui::style::{Modifier, Style}; +use tui::style::Modifier; use tui::text::{Span, Spans, Text}; -use tui::widgets::Clear; use tui::widgets::Paragraph; use tui::widgets::Row; use tui::widgets::Table; use tui::widgets::Tabs; use tui::widgets::{Block, Borders, Wrap}; +use tui::widgets::{Clear, List, ListItem}; use tui::Frame; use crate::app::App; -use crate::models::{Route, StatefulTable, TabState}; +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, logo_block, style_default_bold, style_failure, style_help, - style_highlight, style_primary, style_secondary, style_system_function, title_block, - title_block_centered, vertical_chunks_with_margin, + layout_block_top_border, layout_button_paragraph, layout_button_paragraph_borderless, logo_block, + style_button_highlight, style_default_bold, style_failure, style_help, style_highlight, + style_primary, style_secondary, style_system_function, title_block, title_block_centered, + vertical_chunks_with_margin, }; mod radarr_ui; @@ -150,6 +151,16 @@ pub fn draw_large_popup_over( draw_popup_over(f, app, area, background_fn, popup_fn, 75, 75); } +pub fn draw_drop_down_popup( + f: &mut Frame<'_, B>, + app: &mut App, + area: Rect, + background_fn: fn(&mut Frame<'_, B>, &mut App, Rect), + drop_down_fn: fn(&mut Frame<'_, B>, &mut App, Rect), +) { + draw_popup_over(f, app, area, background_fn, drop_down_fn, 20, 30); +} + fn draw_context_row(f: &mut Frame<'_, B>, app: &App, area: Rect) { if let Route::Radarr(_) = app.get_current_route() { radarr_ui::draw_radarr_context_row(f, app, area) @@ -286,21 +297,36 @@ pub fn draw_prompt_box( } pub fn draw_button(f: &mut Frame<'_, B>, area: Rect, label: &str, is_selected: bool) { - let style = if is_selected { - style_system_function().add_modifier(Modifier::BOLD) - } else { - style_default_bold() - }; - - let label_paragraph = Paragraph::new(Text::from(label)) - .block(layout_block()) - .alignment(Alignment::Center) - .style(style); + let label_paragraph = layout_button_paragraph(is_selected, label, Alignment::Center); f.render_widget(label_paragraph, area); } -pub fn draw_drop_down_menu( +pub fn draw_button_with_icon( + f: &mut Frame<'_, B>, + area: Rect, + label: &str, + icon: &str, + is_selected: bool, +) { + let label_paragraph = layout_button_paragraph_borderless(is_selected, label, Alignment::Left); + let icon_paragraph = layout_button_paragraph_borderless(is_selected, icon, Alignment::Right); + + let horizontal_chunks = horizontal_chunks_with_margin( + vec![Constraint::Percentage(50), Constraint::Percentage(50)], + area, + 1, + ); + + f.render_widget( + layout_block().style(style_button_highlight(is_selected)), + area, + ); + f.render_widget(label_paragraph, horizontal_chunks[0]); + f.render_widget(icon_paragraph, horizontal_chunks[1]); +} + +pub fn draw_drop_down_menu_button( f: &mut Frame<'_, B>, area: Rect, description: &str, @@ -319,10 +345,19 @@ pub fn draw_drop_down_menu( f.render_widget(description_paragraph, horizontal_chunks[0]); - draw_button( - f, - horizontal_chunks[1], - format!("{} ▼", selection).as_str(), - is_selected, - ); + draw_button_with_icon(f, horizontal_chunks[1], selection, "▼", is_selected); +} + +pub fn draw_drop_down_list<'a, B: Backend, T>( + f: &mut Frame<'_, B>, + area: Rect, + content: &'a mut StatefulList, + item_mapper: impl Fn(&T) -> ListItem<'a>, +) { + let items: Vec> = content.items.iter().map(item_mapper).collect(); + let list = List::new(items) + .block(layout_block()) + .highlight_style(style_highlight()); + + f.render_stateful_widget(list, area, &mut content.state); } diff --git a/src/ui/radarr_ui/add_movie_ui.rs b/src/ui/radarr_ui/add_movie_ui.rs index af18681..a0de545 100644 --- a/src/ui/radarr_ui/add_movie_ui.rs +++ b/src/ui/radarr_ui/add_movie_ui.rs @@ -2,16 +2,20 @@ use tui::backend::Backend; use tui::layout::{Alignment, Constraint, Rect}; use tui::style::Modifier; use tui::text::Text; -use tui::widgets::{Cell, Paragraph, Row, Wrap}; +use tui::widgets::{Cell, ListItem, Paragraph, Row, Wrap}; use tui::Frame; use crate::app::radarr::ActiveRadarrBlock; +use crate::models::radarr_models::AddMovieSearchResult; use crate::models::Route; use crate::ui::utils::{ - borderless_block, horizontal_chunks, layout_block, show_cursor, style_default, style_help, - style_primary, title_block_centered, vertical_chunks_with_margin, + borderless_block, get_width, horizontal_chunks, layout_block, 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, }; -use crate::ui::{draw_button, draw_drop_down_menu, draw_medium_popup_over, draw_table, TableProps}; use crate::utils::convert_runtime; use crate::App; @@ -25,14 +29,11 @@ pub(super) fn draw_add_movie_search_popup( 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, - ); + ActiveRadarrBlock::AddMoviePrompt + | ActiveRadarrBlock::AddMovieSelectMonitor + | ActiveRadarrBlock::AddMovieSelectMinimumAvailability + | ActiveRadarrBlock::AddMovieSelectQualityProfile => { + draw_medium_popup_over(f, app, area, draw_add_movie_search, draw_confirmation_popup); } _ => (), } @@ -40,6 +41,16 @@ pub(super) fn draw_add_movie_search_popup( } fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let current_selection = if app.data.radarr_data.add_searched_movies.items.is_empty() { + AddMovieSearchResult::default() + } else { + app + .data + .radarr_data + .add_searched_movies + .current_selection_clone() + }; + let chunks = vertical_chunks_with_margin( vec![ Constraint::Length(3), @@ -68,7 +79,11 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: .alignment(Alignment::Center); f.render_widget(help_paragraph, chunks[2]); } - ActiveRadarrBlock::AddMovieSearchResults | ActiveRadarrBlock::AddMoviePrompt => { + ActiveRadarrBlock::AddMovieSearchResults + | ActiveRadarrBlock::AddMoviePrompt + | ActiveRadarrBlock::AddMovieSelectMonitor + | ActiveRadarrBlock::AddMovieSelectMinimumAvailability + | ActiveRadarrBlock::AddMovieSelectQualityProfile => { let mut help_text = Text::from(" edit search"); help_text.patch_style(style_help()); let help_paragraph = Paragraph::new(help_text) @@ -86,16 +101,16 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: "Title", "Year", "Runtime", - "IMDB Rating", - "Rotten Tomatoes Rating", + "IMDB", + "Rotten Tomatoes", "Genres", ], constraints: vec![ - Constraint::Percentage(20), + Constraint::Percentage(27), Constraint::Percentage(8), Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(18), + Constraint::Percentage(8), + Constraint::Percentage(14), Constraint::Percentage(30), ], }, @@ -128,8 +143,12 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: format!("{}%", rotten_tomatoes_rating) }; + movie + .title + .scroll_or_reset(get_width(area), *movie == current_selection); + Row::new(vec![ - Cell::from(movie.title.to_owned()), + Cell::from(movie.title.to_string()), Cell::from(movie.year.as_u64().unwrap().to_string()), Cell::from(format!("{}h {}m", hours, minutes)), Cell::from(imdb_rating), @@ -148,33 +167,78 @@ fn draw_add_movie_search(f: &mut Frame<'_, B>, app: &mut App, area: f.render_widget(search_paragraph, chunks[0]); } -fn draw_add_movie_confirmation_popup( - f: &mut Frame<'_, B>, - app: &mut App, - prompt_area: Rect, -) { +fn draw_confirmation_popup(f: &mut Frame<'_, B>, app: &mut App, prompt_area: Rect) { if let Route::Radarr(active_radarr_block) = app.get_current_route().clone() { match active_radarr_block { ActiveRadarrBlock::AddMovieSelectMonitor => { - // draw_small_popup_over(f, app, prompt_area, draw_add_movie_confirmation_prompt, draw_add_movie_select_monitor); + draw_drop_down_popup( + f, + app, + prompt_area, + draw_confirmation_prompt, + draw_select_monitor_popup, + ); } ActiveRadarrBlock::AddMovieSelectMinimumAvailability => { - // draw_small_popup_over(f, app, prompt_area, draw_add_movie_confirmation_prompt, draw_add_movie_select_minimum_availability); + draw_drop_down_popup( + f, + app, + prompt_area, + draw_confirmation_prompt, + draw_select_minimum_availability_popup, + ); } ActiveRadarrBlock::AddMovieSelectQualityProfile => { - // draw_small_popup_over(f, app, prompt_area, draw_add_movie_confirmation_prompt, draw_add_movie_select_quality_profile); + draw_drop_down_popup( + f, + app, + prompt_area, + draw_confirmation_prompt, + draw_select_quality_profile_popup, + ); } - ActiveRadarrBlock::AddMoviePrompt => draw_add_movie_confirmation_prompt(f, app, prompt_area), + ActiveRadarrBlock::AddMoviePrompt => draw_confirmation_prompt(f, app, prompt_area), _ => (), } } } -fn draw_add_movie_confirmation_prompt( +fn draw_select_monitor_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_monitor_list, + |monitor| ListItem::new(monitor.to_display_str().to_owned()), + ); +} + +fn draw_select_minimum_availability_popup( f: &mut Frame<'_, B>, app: &mut App, - prompt_area: Rect, + 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 = " Confirm Add Movie? "; let prompt = format!( "{}:\n\n{}", @@ -183,7 +247,9 @@ fn draw_add_movie_confirmation_prompt( .radarr_data .add_searched_movies .current_selection() - .title, + .title + .to_string() + .trim(), app .data .radarr_data @@ -238,7 +304,7 @@ fn draw_add_movie_confirmation_prompt( chunks[5], ); - draw_drop_down_menu( + draw_drop_down_menu_button( f, chunks[1], "Monitor", @@ -246,14 +312,14 @@ fn draw_add_movie_confirmation_prompt( *selected_block == ActiveRadarrBlock::AddMovieSelectMonitor, ); - draw_drop_down_menu( + draw_drop_down_menu_button( f, chunks[2], "Minimum Availability", selected_minimum_availability.to_display_str(), *selected_block == ActiveRadarrBlock::AddMovieSelectMinimumAvailability, ); - draw_drop_down_menu( + draw_drop_down_menu_button( f, chunks[3], "Quality Profile", diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index d34458f..55d10a2 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -18,7 +18,7 @@ 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, + borderless_block, get_width, 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, @@ -257,7 +257,6 @@ fn draw_downloads(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { } else { app.data.radarr_data.downloads.current_selection_clone() }; - let width = (area.width as f32 * 0.30) as usize; draw_table( f, @@ -293,11 +292,7 @@ fn draw_downloads(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { .. } = download_record; - if current_selection == *download_record && output_path.text.len() > width { - output_path.scroll_text() - } else { - output_path.reset_offset(); - } + output_path.scroll_or_reset(get_width(area), current_selection == *download_record); let percent = 1f64 - (sizeleft.as_f64().unwrap() / size.as_f64().unwrap()); let file_size: f64 = convert_to_gb(size.as_u64().unwrap()); diff --git a/src/ui/utils.rs b/src/ui/utils.rs index e6c5281..b8c0ce3 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -1,8 +1,8 @@ use tui::backend::Backend; 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}; +use tui::text::{Span, Spans, Text}; +use tui::widgets::{Block, Borders, LineGauge, Paragraph}; use tui::{symbols, Frame}; pub fn horizontal_chunks(constraints: Vec, size: Rect) -> Vec { @@ -65,6 +65,24 @@ pub fn layout_block_bottom_border<'a>() -> Block<'a> { Block::default().borders(Borders::BOTTOM) } +pub fn layout_button_paragraph(is_selected: bool, label: &str, alignment: Alignment) -> Paragraph { + Paragraph::new(Text::from(label)) + .block(layout_block()) + .alignment(alignment) + .style(style_button_highlight(is_selected)) +} + +pub fn layout_button_paragraph_borderless( + is_selected: bool, + label: &str, + alignment: Alignment, +) -> Paragraph { + Paragraph::new(Text::from(label)) + .block(borderless_block()) + .alignment(alignment) + .style(style_button_highlight(is_selected)) +} + pub fn borderless_block<'a>() -> Block<'a> { Block::default() } @@ -138,6 +156,14 @@ pub fn style_help() -> Style { Style::default().fg(Color::LightBlue) } +pub fn style_button_highlight(is_selected: bool) -> Style { + if is_selected { + style_system_function().add_modifier(Modifier::BOLD) + } else { + style_default_bold() + } +} + pub fn title_style(title: &str) -> Span<'_> { Span::styled(title, style_bold()) } @@ -204,3 +230,7 @@ pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge { pub fn show_cursor(f: &mut Frame<'_, B>, area: Rect, string: &str) { f.set_cursor(area.x + string.len() as u16 + 1, area.y + 1); } + +pub fn get_width(area: Rect) -> usize { + (area.width as f32 * 0.30) as usize +}