From 32a8a4de76db6d48d922411ff068df01f7bbb2c3 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 14 Feb 2024 14:13:00 -0700 Subject: [PATCH] Implemented the final widget for confirmation prompts! --- src/ui/mod.rs | 112 +------------- src/ui/radarr_ui/collections/mod.rs | 36 ++--- src/ui/radarr_ui/downloads/mod.rs | 67 +++----- src/ui/radarr_ui/indexers/mod.rs | 54 +++---- src/ui/radarr_ui/library/delete_movie_ui.rs | 62 +++----- src/ui/radarr_ui/library/mod.rs | 36 ++--- src/ui/radarr_ui/library/movie_details_ui.rs | 113 ++++++-------- src/ui/radarr_ui/root_folders/mod.rs | 40 ++--- src/ui/radarr_ui/system/system_details_ui.rs | 81 ++++------ src/ui/widgets/checkbox.rs | 1 + src/ui/widgets/confirmation_prompt.rs | 154 +++++++++++++++++++ src/ui/widgets/confirmation_prompt_tests.rs | 93 +++++++++++ src/ui/widgets/managarr_table_tests.rs | 4 +- src/ui/widgets/mod.rs | 1 + 14 files changed, 450 insertions(+), 404 deletions(-) create mode 100644 src/ui/widgets/confirmation_prompt.rs create mode 100644 src/ui/widgets/confirmation_prompt_tests.rs diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8a04872..abb6cb9 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,5 +1,3 @@ -use std::iter; - use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::{Line, Text}; @@ -14,11 +12,8 @@ use crate::models::{HorizontallyScrollableText, Route, TabState}; use crate::ui::radarr_ui::RadarrUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ - background_block, borderless_block, centered_rect, layout_paragraph_borderless, logo_block, - title_block, title_block_centered, + background_block, borderless_block, centered_rect, logo_block, title_block, title_block_centered, }; -use crate::ui::widgets::button::Button; -use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::input_box::InputBox; use crate::ui::widgets::popup::Size; @@ -181,111 +176,6 @@ fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) - content_area } -pub fn draw_prompt_box( - f: &mut Frame<'_>, - area: Rect, - title: &str, - prompt: &str, - yes_no_value: bool, -) { - draw_prompt_box_with_content(f, area, title, prompt, None, yes_no_value); -} - -pub fn draw_prompt_box_with_content( - f: &mut Frame<'_>, - area: Rect, - title: &str, - prompt: &str, - content: Option>, - yes_no_value: bool, -) { - f.render_widget(title_block_centered(title), area); - - let [prompt_area, buttons_area] = if let Some(content_paragraph) = content { - let [prompt_area, content_area, _, buttons_area] = Layout::vertical([ - Constraint::Length(4), - Constraint::Length(7), - Constraint::Fill(0), - Constraint::Length(3), - ]) - .margin(1) - .areas(area); - - f.render_widget(content_paragraph, content_area); - - [prompt_area, buttons_area] - } else { - let [prompt_area, _, buttons_area] = Layout::vertical([ - Constraint::Percentage(72), - Constraint::Fill(0), - Constraint::Length(3), - ]) - .margin(1) - .areas(area); - - [prompt_area, buttons_area] - }; - - let prompt_paragraph = layout_paragraph_borderless(prompt); - f.render_widget(prompt_paragraph, prompt_area); - - let [yes_area, no_area] = - Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) - .areas(buttons_area); - - let yes_button = Button::new().title("Yes").selected(yes_no_value); - let no_button = Button::new().title("No").selected(!yes_no_value); - - f.render_widget(yes_button, yes_area); - f.render_widget(no_button, no_area); -} - -pub fn draw_prompt_box_with_checkboxes( - f: &mut Frame<'_>, - area: Rect, - title: &str, - prompt: &str, - checkboxes: Vec<(&str, bool, bool)>, - highlight_yes_no: bool, - yes_no_value: bool, -) { - let mut constraints = vec![ - Constraint::Length(4), - Constraint::Fill(0), - Constraint::Length(3), - ]; - constraints.splice( - 1..1, - iter::repeat(Constraint::Length(3)).take(checkboxes.len()), - ); - let chunks = Layout::vertical(constraints).margin(1).split(area); - let prompt_paragraph = layout_paragraph_borderless(prompt); - - for i in 0..checkboxes.len() { - let (label, is_checked, is_highlighted) = checkboxes[i]; - let checkbox = Checkbox::new(label) - .checked(is_checked) - .highlighted(is_highlighted); - f.render_widget(checkbox, chunks[i + 1]); - } - - let [yes_area, no_area] = - Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) - .areas(chunks[checkboxes.len() + 2]); - - let yes_button = Button::new() - .title("Yes") - .selected(yes_no_value && highlight_yes_no); - let no_button = Button::new() - .title("No") - .selected(!yes_no_value && highlight_yes_no); - - f.render_widget(title_block_centered(title), area); - f.render_widget(prompt_paragraph, chunks[0]); - f.render_widget(yes_button, yes_area); - f.render_widget(no_button, no_area); -} - pub fn draw_input_box_popup( f: &mut Frame<'_>, area: Rect, diff --git a/src/ui/radarr_ui/collections/mod.rs b/src/ui/radarr_ui/collections/mod.rs index 54c7cca..b37c14a 100644 --- a/src/ui/radarr_ui/collections/mod.rs +++ b/src/ui/radarr_ui/collections/mod.rs @@ -12,10 +12,11 @@ use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsU use crate::ui::radarr_ui::collections::edit_collection_ui::EditCollectionUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::error_message::ErrorMessage; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::popup::{Popup, Size}; -use crate::ui::{draw_input_box_popup, draw_popup_over, draw_prompt_box, DrawUi}; +use crate::ui::{draw_input_box_popup, draw_popup_over, DrawUi}; mod collection_details_ui; #[cfg(test)] @@ -49,8 +50,9 @@ impl DrawUi for CollectionsUi { Size::InputBox, ), ActiveRadarrBlock::SearchCollectionError => { - draw_collections(f, app, area); let popup = Popup::new(ErrorMessage::new("Collection not found!")).size(Size::Error); + + draw_collections(f, app, area); f.render_widget(popup, f.size()); } ActiveRadarrBlock::FilterCollections => draw_popup_over( @@ -62,21 +64,23 @@ impl DrawUi for CollectionsUi { Size::InputBox, ), ActiveRadarrBlock::FilterCollectionsError => { - draw_collections(f, app, area); let popup = Popup::new(ErrorMessage::new( "No collections found matching the given filter!", )) .size(Size::Error); + + draw_collections(f, app, area); f.render_widget(popup, f.size()); } - ActiveRadarrBlock::UpdateAllCollectionsPrompt => draw_popup_over( - f, - app, - area, - draw_collections, - draw_update_all_collections_prompt, - Size::Prompt, - ), + ActiveRadarrBlock::UpdateAllCollectionsPrompt => { + let confirmation_prompt = ConfirmationPrompt::new() + .title("Update All Collections") + .prompt("Do you want to update all of your collections?") + .yes_no_value(app.data.radarr_data.prompt_confirm); + + draw_collections(f, app, area); + f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size()); + } _ => (), }; @@ -159,16 +163,6 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) f.render_widget(collections_table, area); } -fn draw_update_all_collections_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_prompt_box( - f, - area, - "Update All Collections", - "Do you want to update all of your collections?", - app.data.radarr_data.prompt_confirm, - ); -} - fn draw_collection_search_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { draw_input_box_popup( f, diff --git a/src/ui/radarr_ui/downloads/mod.rs b/src/ui/radarr_ui/downloads/mod.rs index f9af419..e2172dc 100644 --- a/src/ui/radarr_ui/downloads/mod.rs +++ b/src/ui/radarr_ui/downloads/mod.rs @@ -8,9 +8,10 @@ use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLO use crate::models::{HorizontallyScrollableText, Route}; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::managarr_table::ManagarrTable; -use crate::ui::widgets::popup::Size; -use crate::ui::{draw_popup_over, draw_prompt_box, DrawUi}; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::DrawUi; use crate::utils::convert_to_gb; #[cfg(test)] @@ -32,22 +33,28 @@ impl DrawUi for DownloadsUi { if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { match active_radarr_block { ActiveRadarrBlock::Downloads => draw_downloads(f, app, area), - ActiveRadarrBlock::DeleteDownloadPrompt => draw_popup_over( - f, - app, - area, - draw_downloads, - draw_delete_download_prompt, - Size::Prompt, - ), - ActiveRadarrBlock::UpdateDownloadsPrompt => draw_popup_over( - f, - app, - area, - draw_downloads, - draw_update_downloads_prompt, - Size::Prompt, - ), + ActiveRadarrBlock::DeleteDownloadPrompt => { + let prompt = format!( + "Do you really want to delete this download: \n{}?", + app.data.radarr_data.downloads.current_selection().title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Cancel Download") + .prompt(&prompt) + .yes_no_value(app.data.radarr_data.prompt_confirm); + + draw_downloads(f, app, area); + f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size()); + } + ActiveRadarrBlock::UpdateDownloadsPrompt => { + let confirmation_prompt = ConfirmationPrompt::new() + .title("Update Downloads") + .prompt("Do you want to update your downloads?") + .yes_no_value(app.data.radarr_data.prompt_confirm); + + draw_downloads(f, app, area); + f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size()); + } _ => (), } } @@ -129,27 +136,3 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f.render_widget(downloads_table, area); } - -fn draw_delete_download_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_prompt_box( - f, - area, - "Cancel Download", - format!( - "Do you really want to delete this download: \n{}?", - app.data.radarr_data.downloads.current_selection().title - ) - .as_str(), - app.data.radarr_data.prompt_confirm, - ); -} - -fn draw_update_downloads_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_prompt_box( - f, - area, - "Update Downloads", - "Do you want to update your downloads?", - app.data.radarr_data.prompt_confirm, - ); -} diff --git a/src/ui/radarr_ui/indexers/mod.rs b/src/ui/radarr_ui/indexers/mod.rs index b96dba9..ffa00b4 100644 --- a/src/ui/radarr_ui/indexers/mod.rs +++ b/src/ui/radarr_ui/indexers/mod.rs @@ -12,9 +12,10 @@ use crate::ui::radarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi; use crate::ui::radarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::layout_block_top_border; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::managarr_table::ManagarrTable; -use crate::ui::widgets::popup::Size; -use crate::ui::{draw_popup_over, draw_prompt_box, DrawUi}; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::DrawUi; mod edit_indexer_ui; mod indexer_settings_ui; @@ -42,14 +43,26 @@ impl DrawUi for IndexersUi { let route = *app.get_current_route(); let mut indexers_matchers = |active_radarr_block| match active_radarr_block { ActiveRadarrBlock::Indexers => draw_indexers(f, app, area), - ActiveRadarrBlock::DeleteIndexerPrompt => draw_popup_over( - f, - app, - area, - draw_indexers, - draw_delete_indexer_prompt, - Size::Prompt, - ), + ActiveRadarrBlock::DeleteIndexerPrompt => { + let prompt = format!( + "Do you really want to delete this indexer: \n{}?", + app + .data + .radarr_data + .indexers + .current_selection() + .name + .clone() + .unwrap_or_default() + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Indexer") + .prompt(&prompt) + .yes_no_value(app.data.radarr_data.prompt_confirm); + + draw_indexers(f, app, area); + f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size()); + } _ => (), }; @@ -142,24 +155,3 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f.render_widget(indexers_table, area); } - -fn draw_delete_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_prompt_box( - f, - area, - "Delete Indexer", - format!( - "Do you really want to delete this indexer: \n{}?", - app - .data - .radarr_data - .indexers - .current_selection() - .name - .clone() - .unwrap_or_default() - ) - .as_str(), - app.data.radarr_data.prompt_confirm, - ); -} diff --git a/src/ui/radarr_ui/library/delete_movie_ui.rs b/src/ui/radarr_ui/library/delete_movie_ui.rs index 63b330d..0a67dd6 100644 --- a/src/ui/radarr_ui/library/delete_movie_ui.rs +++ b/src/ui/radarr_ui/library/delete_movie_ui.rs @@ -5,8 +5,10 @@ use crate::app::App; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DELETE_MOVIE_BLOCKS}; use crate::models::Route; use crate::ui::radarr_ui::library::draw_library; -use crate::ui::widgets::popup::Size; -use crate::ui::{draw_popup_over, draw_prompt_box_with_checkboxes, DrawUi}; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::DrawUi; #[cfg(test)] #[path = "delete_movie_ui_tests.rs"] @@ -28,42 +30,28 @@ impl DrawUi for DeleteMovieUi { *app.get_current_route(), Route::Radarr(ActiveRadarrBlock::DeleteMoviePrompt, _) ) { - let draw_delete_movie_prompt = |f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect| { - let selected_block = app.data.radarr_data.selected_block.get_active_block(); - draw_prompt_box_with_checkboxes( - f, - prompt_area, - "Delete Movie", - format!( - "Do you really want to delete: \n{}?", - app.data.radarr_data.movies.current_selection().title.text - ) - .as_str(), - vec![ - ( - "Delete Movie Files", - app.data.radarr_data.delete_movie_files, - selected_block == &ActiveRadarrBlock::DeleteMovieToggleDeleteFile, - ), - ( - "Add List Exclusion", - app.data.radarr_data.add_list_exclusion, - selected_block == &ActiveRadarrBlock::DeleteMovieToggleAddListExclusion, - ), - ], - selected_block == &ActiveRadarrBlock::DeleteMovieConfirmPrompt, - app.data.radarr_data.prompt_confirm, - ) - }; - - draw_popup_over( - f, - app, - area, - draw_library, - draw_delete_movie_prompt, - Size::Prompt, + let selected_block = app.data.radarr_data.selected_block.get_active_block(); + let prompt = format!( + "Do you really want to delete: \n{}?", + app.data.radarr_data.movies.current_selection().title.text ); + let checkboxes = vec![ + Checkbox::new("Delete Movie File") + .checked(app.data.radarr_data.delete_movie_files) + .highlighted(selected_block == &ActiveRadarrBlock::DeleteMovieToggleDeleteFile), + Checkbox::new("Add List Exclusion") + .checked(app.data.radarr_data.add_list_exclusion) + .highlighted(selected_block == &ActiveRadarrBlock::DeleteMovieToggleAddListExclusion), + ]; + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Movie") + .prompt(&prompt) + .checkboxes(checkboxes) + .yes_no_highlighted(selected_block == &ActiveRadarrBlock::DeleteMovieConfirmPrompt) + .yes_no_value(app.data.radarr_data.prompt_confirm); + + draw_library(f, app, area); + f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size()); } } } diff --git a/src/ui/radarr_ui/library/mod.rs b/src/ui/radarr_ui/library/mod.rs index 6e6b2c6..31827b4 100644 --- a/src/ui/radarr_ui/library/mod.rs +++ b/src/ui/radarr_ui/library/mod.rs @@ -12,10 +12,11 @@ use crate::ui::radarr_ui::library::delete_movie_ui::DeleteMovieUi; use crate::ui::radarr_ui::library::edit_movie_ui::EditMovieUi; use crate::ui::radarr_ui::library::movie_details_ui::MovieDetailsUi; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::error_message::ErrorMessage; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::popup::{Popup, Size}; -use crate::ui::{draw_input_box_popup, draw_popup_over, draw_prompt_box, DrawUi}; +use crate::ui::{draw_input_box_popup, draw_popup_over, DrawUi}; use crate::utils::{convert_runtime, convert_to_gb}; mod add_movie_ui; @@ -56,8 +57,9 @@ impl DrawUi for LibraryUi { Size::InputBox, ), ActiveRadarrBlock::SearchMovieError => { - draw_library(f, app, area); let popup = Popup::new(ErrorMessage::new("Movie not found!")).size(Size::Error); + + draw_library(f, app, area); f.render_widget(popup, f.size()); } ActiveRadarrBlock::FilterMovies => draw_popup_over( @@ -69,21 +71,23 @@ impl DrawUi for LibraryUi { Size::InputBox, ), ActiveRadarrBlock::FilterMoviesError => { - draw_library(f, app, area); let popup = Popup::new(ErrorMessage::new( "No movies found matching the given filter!", )) .size(Size::Error); + + draw_library(f, app, area); f.render_widget(popup, f.size()); } - ActiveRadarrBlock::UpdateAllMoviesPrompt => draw_popup_over( - f, - app, - area, - draw_library, - draw_update_all_movies_prompt, - Size::Prompt, - ), + ActiveRadarrBlock::UpdateAllMoviesPrompt => { + let confirmation_prompt = ConfirmationPrompt::new() + .title("Update All Movies") + .prompt("Do you want to update info and scan your disks for all of your movies?") + .yes_no_value(app.data.radarr_data.prompt_confirm); + + draw_library(f, app, area); + f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size()); + } _ => (), }; @@ -194,16 +198,6 @@ pub(super) fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } } -fn draw_update_all_movies_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_prompt_box( - f, - area, - "Update All Movies", - "Do you want to update info and scan your disks for all of your movies?", - app.data.radarr_data.prompt_confirm, - ); -} - fn draw_movie_search_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { draw_input_box_popup( f, diff --git a/src/ui/radarr_ui/library/movie_details_ui.rs b/src/ui/radarr_ui/library/movie_details_ui.rs index ea4d740..2888e59 100644 --- a/src/ui/radarr_ui/library/movie_details_ui.rs +++ b/src/ui/radarr_ui/library/movie_details_ui.rs @@ -15,12 +15,11 @@ use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ borderless_block, get_width_from_percentage, layout_block_bottom_border, layout_block_top_border, }; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::managarr_table::ManagarrTable; -use crate::ui::widgets::popup::Size; -use crate::ui::{ - draw_popup_over, draw_prompt_box, draw_prompt_box_with_content, draw_tabs, DrawUi, -}; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::{draw_popup_over, draw_tabs, DrawUi}; use crate::utils::convert_to_gb; #[cfg(test)] @@ -47,33 +46,38 @@ impl DrawUi for MovieDetailsUi { "Movie Info", &app.data.radarr_data.movie_info_tabs, ); + draw_movie_info(f, app, content_area); match context_option.unwrap_or(active_radarr_block) { - ActiveRadarrBlock::AutomaticallySearchMoviePrompt => draw_popup_over( - f, - app, - content_area, - draw_movie_info, - draw_search_movie_prompt, - Size::Prompt, - ), - ActiveRadarrBlock::UpdateAndScanPrompt => draw_popup_over( - f, - app, - content_area, - draw_movie_info, - draw_update_and_scan_prompt, - Size::Prompt, - ), - ActiveRadarrBlock::ManualSearchConfirmPrompt => draw_popup_over( - f, - app, - content_area, - draw_movie_info, - draw_manual_search_confirm_prompt, - Size::Small, - ), - _ => draw_movie_info(f, app, content_area), + ActiveRadarrBlock::AutomaticallySearchMoviePrompt => { + let prompt = format!( + "Do you want to trigger an automatic search of your indexers for the movie: {}?", + app.data.radarr_data.movies.current_selection().title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Automatic Movie Search") + .prompt(&prompt) + .yes_no_value(app.data.radarr_data.prompt_confirm); + + draw_movie_info(f, app, content_area); + f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size()); + } + ActiveRadarrBlock::UpdateAndScanPrompt => { + let prompt = format!( + "Do you want to trigger an update and disk scan for the movie: {}?", + app.data.radarr_data.movies.current_selection().title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Update and Scan") + .prompt(&prompt) + .yes_no_value(app.data.radarr_data.prompt_confirm); + + f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size()); + } + ActiveRadarrBlock::ManualSearchConfirmPrompt => { + draw_manual_search_confirm_prompt(f, app); + } + _ => (), } }; @@ -105,34 +109,6 @@ fn draw_movie_info(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } } -fn draw_search_movie_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_prompt_box( - f, - area, - "Automatic Movie Search", - format!( - "Do you want to trigger an automatic search of your indexers for the movie: {}?", - app.data.radarr_data.movies.current_selection().title - ) - .as_str(), - app.data.radarr_data.prompt_confirm, - ); -} - -fn draw_update_and_scan_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_prompt_box( - f, - area, - "Update and Scan", - format!( - "Do you want to trigger an update and disk scan for the movie: {}?", - app.data.radarr_data.movies.current_selection().title - ) - .as_str(), - app.data.radarr_data.prompt_confirm, - ); -} - fn draw_file_info(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { match app.data.radarr_data.movie_details_modal.as_ref() { Some(movie_details_modal) @@ -484,7 +460,7 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } } -fn draw_manual_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { +fn draw_manual_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>) { let current_selection = app .data .radarr_data @@ -525,17 +501,20 @@ fn draw_manual_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: .block(borderless_block()) .wrap(Wrap { trim: false }) .alignment(Alignment::Left); + let confirmation_prompt = ConfirmationPrompt::new() + .title(title) + .prompt(&prompt) + .content(content_paragraph) + .yes_no_value(app.data.radarr_data.prompt_confirm); - draw_prompt_box_with_content( - f, - area, - title, - &prompt, - Some(content_paragraph), - app.data.radarr_data.prompt_confirm, - ); + f.render_widget(Popup::new(confirmation_prompt).size(Size::Small), f.size()); } else { - draw_prompt_box(f, area, title, &prompt, app.data.radarr_data.prompt_confirm); + let confirmation_prompt = ConfirmationPrompt::new() + .title(title) + .prompt(&prompt) + .yes_no_value(app.data.radarr_data.prompt_confirm); + + f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size()); } } diff --git a/src/ui/radarr_ui/root_folders/mod.rs b/src/ui/radarr_ui/root_folders/mod.rs index 7e070e1..8f4837a 100644 --- a/src/ui/radarr_ui/root_folders/mod.rs +++ b/src/ui/radarr_ui/root_folders/mod.rs @@ -8,9 +8,10 @@ use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_F use crate::models::Route; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::layout_block_top_border; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::managarr_table::ManagarrTable; -use crate::ui::widgets::popup::Size; -use crate::ui::{draw_input_box_popup, draw_popup_over, draw_prompt_box, DrawUi}; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::{draw_input_box_popup, draw_popup_over, DrawUi}; use crate::utils::convert_to_gb; #[cfg(test)] @@ -40,14 +41,19 @@ impl DrawUi for RootFoldersUi { draw_add_root_folder_prompt_box, Size::InputBox, ), - ActiveRadarrBlock::DeleteRootFolderPrompt => draw_popup_over( - f, - app, - area, - draw_root_folders, - draw_delete_root_folder_prompt, - Size::Prompt, - ), + ActiveRadarrBlock::DeleteRootFolderPrompt => { + let prompt = format!( + "Do you really want to delete this root folder: \n{}?", + app.data.radarr_data.root_folders.current_selection().path + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Root Folder") + .prompt(&prompt) + .yes_no_value(app.data.radarr_data.prompt_confirm); + + draw_root_folders(f, app, area); + f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size()); + } _ => (), } } @@ -109,17 +115,3 @@ fn draw_add_root_folder_prompt_box(f: &mut Frame<'_>, app: &mut App<'_>, area: R app.data.radarr_data.edit_root_folder.as_ref().unwrap(), ); } - -fn draw_delete_root_folder_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_prompt_box( - f, - area, - "Delete Root Folder", - format!( - "Do you really want to delete this root folder: \n{}?", - app.data.radarr_data.root_folders.current_selection().path - ) - .as_str(), - app.data.radarr_data.prompt_confirm, - ); -} diff --git a/src/ui/radarr_ui/system/system_details_ui.rs b/src/ui/radarr_ui/system/system_details_ui.rs index 2cfbd7b..2c318bb 100644 --- a/src/ui/radarr_ui/system/system_details_ui.rs +++ b/src/ui/radarr_ui/system/system_details_ui.rs @@ -16,11 +16,12 @@ use crate::ui::radarr_ui::system::{ }; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{borderless_block, title_block}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::popup::{Popup, Size}; use crate::ui::widgets::selectable_list::SelectableList; -use crate::ui::{draw_popup_over, draw_prompt_box, DrawUi}; +use crate::ui::{draw_popup_over, DrawUi}; #[cfg(test)] #[path = "system_details_ui_tests.rs"] @@ -106,62 +107,46 @@ fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) { } fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let tasks_popup_table = |f: &mut Frame<'_>, app: &mut App<'_>, area: Rect| { - let help_footer = Some(build_context_clue_string(&SYSTEM_TASKS_CONTEXT_CLUES)); - let tasks_row_mapping = |task: &Task| { - let task_props = extract_task_props(task); + let help_footer = Some(build_context_clue_string(&SYSTEM_TASKS_CONTEXT_CLUES)); + let tasks_row_mapping = |task: &Task| { + let task_props = extract_task_props(task); - Row::new(vec![ - Cell::from(task_props.name), - Cell::from(task_props.interval), - Cell::from(task_props.last_execution), - Cell::from(task_props.last_duration), - Cell::from(task_props.next_execution), - ]) - .primary() - }; - let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping) - .block(borderless_block()) - .loading(app.is_loading) - .margin(1) - .footer(help_footer) - .footer_alignment(Alignment::Center) - .headers(TASK_TABLE_HEADERS) - .constraints(TASK_TABLE_CONSTRAINTS); - - f.render_widget(title_block("Tasks"), area); - f.render_widget(tasks_table, area); + Row::new(vec![ + Cell::from(task_props.name), + Cell::from(task_props.interval), + Cell::from(task_props.last_execution), + Cell::from(task_props.last_duration), + Cell::from(task_props.next_execution), + ]) + .primary() }; + let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping) + .block(borderless_block()) + .loading(app.is_loading) + .margin(1) + .footer(help_footer) + .footer_alignment(Alignment::Center) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); + + f.render_widget(title_block("Tasks"), area); + f.render_widget(tasks_table, area); if matches!( app.get_current_route(), Route::Radarr(ActiveRadarrBlock::SystemTaskStartConfirmPrompt, _) ) { - draw_popup_over( - f, - app, - area, - tasks_popup_table, - draw_start_task_prompt, - Size::Prompt, - ) - } else { - tasks_popup_table(f, app, area); - } -} - -fn draw_start_task_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - draw_prompt_box( - f, - area, - "Start Task", - format!( + let prompt = format!( "Do you want to manually start this task: {}?", app.data.radarr_data.tasks.current_selection().name - ) - .as_str(), - app.data.radarr_data.prompt_confirm, - ); + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Start Task") + .prompt(&prompt) + .yes_no_value(app.data.radarr_data.prompt_confirm); + + f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size()); + } } fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>) { diff --git a/src/ui/widgets/checkbox.rs b/src/ui/widgets/checkbox.rs index b83f12b..962673b 100644 --- a/src/ui/widgets/checkbox.rs +++ b/src/ui/widgets/checkbox.rs @@ -10,6 +10,7 @@ use ratatui::widgets::{Paragraph, Widget}; #[path = "checkbox_tests.rs"] mod checkbox_tests; +#[derive(PartialEq, Debug, Copy, Clone)] pub struct Checkbox<'a> { label: &'a str, is_checked: bool, diff --git a/src/ui/widgets/confirmation_prompt.rs b/src/ui/widgets/confirmation_prompt.rs new file mode 100644 index 0000000..543557b --- /dev/null +++ b/src/ui/widgets/confirmation_prompt.rs @@ -0,0 +1,154 @@ +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{layout_paragraph_borderless, title_block_centered}; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use ratatui::buffer::Buffer; +use ratatui::layout::{Constraint, Flex, Layout, Rect}; +use ratatui::widgets::{Paragraph, Widget}; +use std::iter; + +#[cfg(test)] +#[path = "confirmation_prompt_tests.rs"] +mod confirmation_prompt_tests; + +pub struct ConfirmationPrompt<'a> { + title: &'a str, + prompt: &'a str, + content: Option>, + checkboxes: Option>>, + yes_no_value: bool, + yes_no_highlighted: bool, +} + +impl<'a> ConfirmationPrompt<'a> { + pub fn new() -> Self { + Self { + title: "", + prompt: "", + content: None, + checkboxes: None, + yes_no_value: false, + yes_no_highlighted: true, + } + } + + pub fn title(mut self, title: &'a str) -> Self { + self.title = title; + self + } + + pub fn prompt(mut self, prompt: &'a str) -> Self { + self.prompt = prompt; + self + } + + pub fn content(mut self, content: Paragraph<'a>) -> Self { + self.content = Some(content); + self + } + + pub fn checkboxes(mut self, checkboxes: Vec>) -> Self { + self.checkboxes = Some(checkboxes); + self + } + + pub fn yes_no_value(mut self, yes_highlighted: bool) -> Self { + self.yes_no_value = yes_highlighted; + self + } + + pub fn yes_no_highlighted(mut self, yes_highlighted: bool) -> Self { + self.yes_no_highlighted = yes_highlighted; + self + } + + fn render_confirmation_prompt_with_checkboxes(self, area: Rect, buf: &mut Buffer) { + title_block_centered(self.title).render(area, buf); + + if let Some(checkboxes) = self.checkboxes { + let mut constraints = vec![ + Constraint::Length(4), + Constraint::Fill(0), + Constraint::Length(3), + ]; + constraints.splice( + 1..1, + iter::repeat(Constraint::Length(3)).take(checkboxes.len()), + ); + let chunks = Layout::vertical(constraints).margin(1).split(area); + let [yes_area, no_area] = + Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) + .areas(chunks[checkboxes.len() + 2]); + + layout_paragraph_borderless(self.prompt).render(chunks[0], buf); + + checkboxes + .into_iter() + .enumerate() + .for_each(|(i, checkbox)| { + checkbox.render(chunks[i + 1], buf); + }); + + Button::new() + .title("Yes") + .selected(self.yes_no_value && self.yes_no_highlighted) + .render(yes_area, buf); + Button::new() + .title("No") + .selected(!self.yes_no_value && self.yes_no_highlighted) + .render(no_area, buf); + } + } + + fn render_confirmation_prompt(self, area: Rect, buf: &mut Buffer) { + title_block_centered(self.title).render(area, buf); + + let [prompt_area, buttons_area] = if let Some(content_paragraph) = self.content { + let [prompt_area, content_area, _, buttons_area] = Layout::vertical([ + Constraint::Length(4), + Constraint::Length(7), + Constraint::Fill(0), + Constraint::Length(3), + ]) + .margin(1) + .areas(area); + + content_paragraph.render(content_area, buf); + + [prompt_area, buttons_area] + } else { + let [prompt_area, buttons_area] = + Layout::vertical([Constraint::Percentage(72), Constraint::Length(3)]) + .margin(1) + .flex(Flex::SpaceBetween) + .areas(area); + + [prompt_area, buttons_area] + }; + + layout_paragraph_borderless(self.prompt).render(prompt_area, buf); + + let [yes_area, no_area] = + Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) + .areas(buttons_area); + + Button::new() + .title("Yes") + .selected(self.yes_no_value) + .render(yes_area, buf); + Button::new() + .title("No") + .selected(!self.yes_no_value) + .render(no_area, buf); + } +} + +impl<'a> Widget for ConfirmationPrompt<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + if self.checkboxes.is_some() { + self.render_confirmation_prompt_with_checkboxes(area, buf); + } else { + self.render_confirmation_prompt(area, buf); + } + } +} diff --git a/src/ui/widgets/confirmation_prompt_tests.rs b/src/ui/widgets/confirmation_prompt_tests.rs new file mode 100644 index 0000000..6173ef7 --- /dev/null +++ b/src/ui/widgets/confirmation_prompt_tests.rs @@ -0,0 +1,93 @@ +#[cfg(test)] +mod tests { + use crate::ui::widgets::checkbox::Checkbox; + use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; + use pretty_assertions::{assert_eq, assert_str_eq}; + use ratatui::widgets::Paragraph; + + #[test] + fn test_confirmation_prompt_new() { + let confirmation_prompt = ConfirmationPrompt::new(); + + assert_str_eq!(confirmation_prompt.title, ""); + assert_str_eq!(confirmation_prompt.prompt, ""); + assert_eq!(confirmation_prompt.content, None); + assert_eq!(confirmation_prompt.checkboxes, None); + assert!(!confirmation_prompt.yes_no_value); + assert!(confirmation_prompt.yes_no_highlighted); + } + + #[test] + fn test_confirmation_prompt_title() { + let confirmation_prompt = ConfirmationPrompt::new().title("title"); + + assert_str_eq!(confirmation_prompt.title, "title"); + assert_str_eq!(confirmation_prompt.prompt, ""); + assert_eq!(confirmation_prompt.content, None); + assert_eq!(confirmation_prompt.checkboxes, None); + assert!(!confirmation_prompt.yes_no_value); + assert!(confirmation_prompt.yes_no_highlighted); + } + + #[test] + fn test_confirmation_prompt_prompt() { + let confirmation_prompt = ConfirmationPrompt::new().prompt("prompt"); + + assert_str_eq!(confirmation_prompt.prompt, "prompt"); + assert_str_eq!(confirmation_prompt.title, ""); + assert_eq!(confirmation_prompt.content, None); + assert_eq!(confirmation_prompt.checkboxes, None); + assert!(!confirmation_prompt.yes_no_value); + assert!(confirmation_prompt.yes_no_highlighted); + } + + #[test] + fn test_confirmation_prompt_content() { + let content = Paragraph::new("content"); + let confirmation_prompt = ConfirmationPrompt::new().content(content.clone()); + + assert_eq!(confirmation_prompt.content, Some(content)); + assert_str_eq!(confirmation_prompt.title, ""); + assert_str_eq!(confirmation_prompt.prompt, ""); + assert_eq!(confirmation_prompt.checkboxes, None); + assert!(!confirmation_prompt.yes_no_value); + assert!(confirmation_prompt.yes_no_highlighted); + } + + #[test] + fn test_confirmation_prompt_checkboxes() { + let checkboxes = vec![Checkbox::new("test").highlighted(true).checked(false)]; + let confirmation_prompt = ConfirmationPrompt::new().checkboxes(checkboxes.clone()); + + assert_eq!(confirmation_prompt.checkboxes, Some(checkboxes)); + assert_str_eq!(confirmation_prompt.title, ""); + assert_str_eq!(confirmation_prompt.prompt, ""); + assert_eq!(confirmation_prompt.content, None); + assert!(!confirmation_prompt.yes_no_value); + assert!(confirmation_prompt.yes_no_highlighted); + } + + #[test] + fn test_confirmation_prompt_yes_no_value() { + let confirmation_prompt = ConfirmationPrompt::new().yes_no_value(true); + + assert!(confirmation_prompt.yes_no_value); + assert_str_eq!(confirmation_prompt.title, ""); + assert_str_eq!(confirmation_prompt.prompt, ""); + assert_eq!(confirmation_prompt.content, None); + assert_eq!(confirmation_prompt.checkboxes, None); + assert!(confirmation_prompt.yes_no_highlighted); + } + + #[test] + fn test_confirmation_prompt_yes_no_highlighted() { + let confirmation_prompt = ConfirmationPrompt::new().yes_no_highlighted(false); + + assert!(!confirmation_prompt.yes_no_highlighted); + assert_str_eq!(confirmation_prompt.title, ""); + assert_str_eq!(confirmation_prompt.prompt, ""); + assert_eq!(confirmation_prompt.content, None); + assert_eq!(confirmation_prompt.checkboxes, None); + assert!(!confirmation_prompt.yes_no_value); + } +} diff --git a/src/ui/widgets/managarr_table_tests.rs b/src/ui/widgets/managarr_table_tests.rs index 1c32f41..16c9951 100644 --- a/src/ui/widgets/managarr_table_tests.rs +++ b/src/ui/widgets/managarr_table_tests.rs @@ -42,7 +42,7 @@ mod tests { let managarr_table = ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .headers(headers.clone()); + .headers(headers); let row_mapper = managarr_table.row_mapper; assert_eq!(managarr_table.table_headers, headers); @@ -67,7 +67,7 @@ mod tests { let managarr_table = ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .constraints(constraints.clone()); + .constraints(constraints); let row_mapper = managarr_table.row_mapper; assert_eq!(managarr_table.constraints, constraints); diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs index 036040a..b0502b0 100644 --- a/src/ui/widgets/mod.rs +++ b/src/ui/widgets/mod.rs @@ -1,5 +1,6 @@ pub(super) mod button; pub(super) mod checkbox; +pub(super) mod confirmation_prompt; pub(super) mod error_message; pub(super) mod input_box; pub(super) mod loading_block;