diff --git a/src/app/app_tests.rs b/src/app/app_tests.rs index c359177..99697b8 100644 --- a/src/app/app_tests.rs +++ b/src/app/app_tests.rs @@ -5,7 +5,6 @@ mod tests { use serial_test::serial; use tokio::sync::mpsc; - use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES}; use crate::app::{interpolate_env_vars, App, AppConfig, Data, ServarrConfig}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; @@ -39,40 +38,24 @@ mod tests { TabRoute { title: "Sonarr Test".to_owned(), route: ActiveSonarrBlock::default().into(), - help: format!( - "<↑↓> scroll | page up/down | ←→ change tab | {} ", - build_context_clue_string(&SERVARR_CONTEXT_CLUES) - ), contextual_help: None, config: Some(sonarr_config_1), }, TabRoute { title: "Radarr 1".to_owned(), route: ActiveRadarrBlock::default().into(), - help: format!( - "<↑↓> scroll | page up/down | ←→ change tab | {} ", - build_context_clue_string(&SERVARR_CONTEXT_CLUES) - ), contextual_help: None, config: Some(radarr_config_2), }, TabRoute { title: "Radarr Test".to_owned(), route: ActiveRadarrBlock::default().into(), - help: format!( - "<↑↓> scroll | page up/down | ←→ change tab | {} ", - build_context_clue_string(&SERVARR_CONTEXT_CLUES) - ), contextual_help: None, config: Some(radarr_config_1), }, TabRoute { title: "Sonarr 1".to_owned(), route: ActiveSonarrBlock::default().into(), - help: format!( - "<↑↓> scroll | page up/down | ←→ change tab | {} ", - build_context_clue_string(&SERVARR_CONTEXT_CLUES) - ), contextual_help: None, config: Some(sonarr_config_2), }, diff --git a/src/app/context_clues.rs b/src/app/context_clues.rs index 7f83a5c..b4a03ab 100644 --- a/src/app/context_clues.rs +++ b/src/app/context_clues.rs @@ -1,25 +1,51 @@ use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS}; +use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider; +use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider; +use crate::app::App; +use crate::models::Route; #[cfg(test)] #[path = "context_clues_tests.rs"] mod context_clues_tests; -pub(in crate::app) type ContextClue = (KeyBinding, &'static str); +pub type ContextClue = (KeyBinding, &'static str); -pub fn build_context_clue_string(context_clues: &[(KeyBinding, &str)]) -> String { - context_clues - .iter() - .map(|(key_binding, desc)| format!("{} {desc}", key_binding.key)) - .collect::>() - .join(" | ") +pub trait ContextClueProvider { + fn get_context_clues(_app: &mut App<'_>) -> Option<&'static [ContextClue]>; } -pub static SERVARR_CONTEXT_CLUES: [ContextClue; 2] = [ +pub struct ServarrContextClueProvider; + +impl ContextClueProvider for ServarrContextClueProvider { + fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> { + match app.get_current_route() { + Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app), + Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app), + _ => None, + } + } +} + +pub static SERVARR_CONTEXT_CLUES: [ContextClue; 10] = [ + (DEFAULT_KEYBINDINGS.up, "scroll up"), + (DEFAULT_KEYBINDINGS.down, "scroll down"), + (DEFAULT_KEYBINDINGS.left, "previous tab"), + (DEFAULT_KEYBINDINGS.right, "next tab"), + (DEFAULT_KEYBINDINGS.pg_up, DEFAULT_KEYBINDINGS.pg_up.desc), + ( + DEFAULT_KEYBINDINGS.pg_down, + DEFAULT_KEYBINDINGS.pg_down.desc, + ), ( DEFAULT_KEYBINDINGS.next_servarr, DEFAULT_KEYBINDINGS.next_servarr.desc, ), + ( + DEFAULT_KEYBINDINGS.previous_servarr, + DEFAULT_KEYBINDINGS.previous_servarr.desc, + ), (DEFAULT_KEYBINDINGS.quit, DEFAULT_KEYBINDINGS.quit.desc), + (DEFAULT_KEYBINDINGS.help, DEFAULT_KEYBINDINGS.help.desc), ]; pub static BARE_POPUP_CONTEXT_CLUES: [ContextClue; 1] = diff --git a/src/app/context_clues_tests.rs b/src/app/context_clues_tests.rs index 962f071..d5be326 100644 --- a/src/app/context_clues_tests.rs +++ b/src/app/context_clues_tests.rs @@ -3,24 +3,15 @@ mod test { use pretty_assertions::{assert_eq, assert_str_eq}; use crate::app::context_clues::{ - BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, - DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, - SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + ContextClueProvider, ServarrContextClueProvider, BARE_POPUP_CONTEXT_CLUES, + BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SERVARR_CONTEXT_CLUES, + SYSTEM_CONTEXT_CLUES, }; - use crate::app::{context_clues::build_context_clue_string, key_binding::DEFAULT_KEYBINDINGS}; - - #[test] - fn test_build_context_clue_string() { - let test_context_clues_array = [ - (DEFAULT_KEYBINDINGS.add, "add"), - (DEFAULT_KEYBINDINGS.delete, "delete"), - ]; - - assert_str_eq!( - build_context_clue_string(&test_context_clues_array), - " add | delete" - ); - } + use crate::app::{key_binding::DEFAULT_KEYBINDINGS, App}; + use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; + use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; + use crate::models::servarr_data::ActiveKeybindingBlock; #[test] fn test_servarr_context_clues() { @@ -28,13 +19,53 @@ mod test { let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.up); + assert_str_eq!(*description, "scroll up"); + + let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.down); + assert_str_eq!(*description, "scroll down"); + + let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.left); + assert_str_eq!(*description, "previous tab"); + + let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.right); + assert_str_eq!(*description, "next tab"); + + let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.pg_up); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.pg_up.desc); + + let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.pg_down); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.pg_down.desc); + + let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.next_servarr); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.next_servarr.desc); let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.previous_servarr); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.previous_servarr.desc); + + let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.quit); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.quit.desc); + + let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.help); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.help.desc); assert_eq!(servarr_context_clues_iter.next(), None); } @@ -204,4 +235,42 @@ mod test { assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); assert_eq!(system_context_clues_iter.next(), None); } + + #[test] + fn test_servarr_context_clue_provider_delegates_to_radarr_provider() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into()); + + let context_clues = ServarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!( + &crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES, + context_clues.unwrap() + ); + } + + #[test] + fn test_servarr_context_clue_provider_delegates_to_sonarr_provider() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into()); + + let context_clues = ServarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!( + &crate::app::sonarr::sonarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES, + context_clues.unwrap() + ); + } + + #[test] + fn test_servarr_context_clue_provider_unsupported_route_returns_none() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveKeybindingBlock::Help.into()); + + let context_clues = ServarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_none()); + } } diff --git a/src/app/key_binding.rs b/src/app/key_binding.rs index 547cf45..d63f9e8 100644 --- a/src/app/key_binding.rs +++ b/src/app/key_binding.rs @@ -23,6 +23,7 @@ generate_keybindings! { search, auto_search, settings, + help, filter, sort, edit, @@ -121,6 +122,11 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { alt: None, desc: "settings", }, + help: KeyBinding { + key: Key::Char('?'), + alt: None, + desc: "show/hide keybindings", + }, filter: KeyBinding { key: Key::Char('f'), alt: None, diff --git a/src/app/key_binding_tests.rs b/src/app/key_binding_tests.rs index c9604ad..a5357f3 100644 --- a/src/app/key_binding_tests.rs +++ b/src/app/key_binding_tests.rs @@ -22,6 +22,7 @@ mod test { #[case(DEFAULT_KEYBINDINGS.auto_search, Key::Char('S'), None, "auto search")] #[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), None, "search")] #[case(DEFAULT_KEYBINDINGS.settings, Key::Char('S'), None, "settings")] + #[case(DEFAULT_KEYBINDINGS.help, Key::Char('?'), None, "show/hide keybindings")] #[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), None, "filter")] #[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), None, "sort")] #[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), None, "edit")] diff --git a/src/app/mod.rs b/src/app/mod.rs index 9220a07..5707f76 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -10,10 +10,11 @@ use tokio::sync::mpsc::Sender; use tokio_util::sync::CancellationToken; use veil::Redact; -use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES}; use crate::cli::Command; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; +use crate::models::servarr_models::KeybindingItem; +use crate::models::stateful_table::StatefulTable; use crate::models::{HorizontallyScrollableText, Route, TabRoute, TabState}; use crate::network::NetworkEvent; @@ -32,6 +33,7 @@ pub struct App<'a> { pub cancellation_token: CancellationToken, pub is_first_render: bool, pub server_tabs: TabState, + pub keymapping_table: Option>, pub error: HorizontallyScrollableText, pub tick_until_poll: u64, pub ticks_until_scroll: u64, @@ -51,10 +53,6 @@ impl App<'_> { cancellation_token: CancellationToken, ) -> Self { let mut server_tabs = Vec::new(); - let help = format!( - "<↑↓> scroll | page up/down | ←→ change tab | {} ", - build_context_clue_string(&SERVARR_CONTEXT_CLUES) - ); if let Some(radarr_configs) = config.radarr { let mut idx = 0; @@ -69,7 +67,6 @@ impl App<'_> { server_tabs.push(TabRoute { title: name, route: ActiveRadarrBlock::Movies.into(), - help: help.clone(), contextual_help: None, config: Some(radarr_config), }); @@ -90,7 +87,6 @@ impl App<'_> { server_tabs.push(TabRoute { title: name, route: ActiveSonarrBlock::Series.into(), - help: help.clone(), contextual_help: None, config: Some(sonarr_config), }); @@ -215,6 +211,7 @@ impl Default for App<'_> { navigation_stack: Vec::new(), network_tx: None, cancellation_token: CancellationToken::new(), + keymapping_table: None, error: HorizontallyScrollableText::default(), is_first_render: true, server_tabs: TabState::new(Vec::new()), @@ -239,20 +236,12 @@ impl App<'_> { TabRoute { title: "Radarr".to_owned(), route: ActiveRadarrBlock::Movies.into(), - help: format!( - "<↑↓> scroll | ←→ change tab | {} ", - build_context_clue_string(&SERVARR_CONTEXT_CLUES) - ), contextual_help: None, config: Some(ServarrConfig::default()), }, TabRoute { title: "Sonarr".to_owned(), route: ActiveSonarrBlock::Series.into(), - help: format!( - "<↑↓> scroll | ←→ change tab | {} ", - build_context_clue_string(&SERVARR_CONTEXT_CLUES) - ), contextual_help: None, config: Some(ServarrConfig::default()), }, diff --git a/src/app/radarr/radarr_context_clues.rs b/src/app/radarr/radarr_context_clues.rs index b63cbbb..68d2823 100644 --- a/src/app/radarr/radarr_context_clues.rs +++ b/src/app/radarr/radarr_context_clues.rs @@ -1,5 +1,13 @@ -use crate::app::context_clues::ContextClue; +use crate::app::context_clues::{ + ContextClue, ContextClueProvider, BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, +}; use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::models::servarr_data::radarr::radarr_data::{ + ActiveRadarrBlock, ADD_MOVIE_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_INDEXER_BLOCKS, + EDIT_MOVIE_BLOCKS, INDEXER_SETTINGS_BLOCKS, MOVIE_DETAILS_BLOCKS, +}; +use crate::models::Route; #[cfg(test)] #[path = "radarr_context_clues_tests.rs"] @@ -53,7 +61,7 @@ pub static MOVIE_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [ (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; -pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 6] = [ +pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 7] = [ ( DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh.desc, @@ -65,12 +73,10 @@ pub static MANUAL_MOVIE_SEARCH_CONTEXT_CLUES: [ContextClue; 6] = [ DEFAULT_KEYBINDINGS.auto_search, DEFAULT_KEYBINDINGS.auto_search.desc, ), + (DEFAULT_KEYBINDINGS.submit, "details"), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; -pub static MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 1] = - [(DEFAULT_KEYBINDINGS.submit, "details")]; - pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [ (DEFAULT_KEYBINDINGS.submit, "details"), (DEFAULT_KEYBINDINGS.esc, "edit search"), @@ -86,3 +92,54 @@ pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [ (DEFAULT_KEYBINDINGS.edit, "edit collection"), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; + +pub(in crate::app) struct RadarrContextClueProvider; + +impl ContextClueProvider for RadarrContextClueProvider { + fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> { + let Route::Radarr(active_radarr_block, context_option) = app.get_current_route() else { + panic!("RadarrContextClueProvider::get_context_clues called with non-Radarr route"); + }; + match active_radarr_block { + _ if MOVIE_DETAILS_BLOCKS.contains(&active_radarr_block) => app + .data + .radarr_data + .movie_info_tabs + .get_active_route_contextual_help(), + ActiveRadarrBlock::TestAllIndexers + | ActiveRadarrBlock::AddMovieSearchInput + | ActiveRadarrBlock::AddMovieEmptySearchResults + | ActiveRadarrBlock::SystemLogs + | ActiveRadarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES), + _ if context_option.unwrap_or(active_radarr_block) + == ActiveRadarrBlock::ViewMovieOverview => + { + Some(&BARE_POPUP_CONTEXT_CLUES) + } + ActiveRadarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES), + _ if EDIT_COLLECTION_BLOCKS.contains(&active_radarr_block) + || EDIT_INDEXER_BLOCKS.contains(&active_radarr_block) + || INDEXER_SETTINGS_BLOCKS.contains(&active_radarr_block) + || EDIT_MOVIE_BLOCKS.contains(&active_radarr_block) => + { + Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES) + } + ActiveRadarrBlock::AddMoviePrompt + | ActiveRadarrBlock::AddMovieSelectMonitor + | ActiveRadarrBlock::AddMovieSelectMinimumAvailability + | ActiveRadarrBlock::AddMovieSelectQualityProfile + | ActiveRadarrBlock::AddMovieSelectRootFolder + | ActiveRadarrBlock::AddMovieTagsInput + | ActiveRadarrBlock::SystemTaskStartConfirmPrompt => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES), + _ if ADD_MOVIE_BLOCKS.contains(&active_radarr_block) => { + Some(&ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES) + } + ActiveRadarrBlock::CollectionDetails => Some(&COLLECTION_DETAILS_CONTEXT_CLUES), + _ => app + .data + .radarr_data + .main_tabs + .get_active_route_contextual_help(), + } + } +} diff --git a/src/app/radarr/radarr_context_clues_tests.rs b/src/app/radarr/radarr_context_clues_tests.rs index b6fbd19..b98c5b5 100644 --- a/src/app/radarr/radarr_context_clues_tests.rs +++ b/src/app/radarr/radarr_context_clues_tests.rs @@ -1,14 +1,21 @@ #[cfg(test)] mod tests { - use pretty_assertions::{assert_eq, assert_str_eq}; - + use crate::app::context_clues::{ + ContextClue, ContextClueProvider, BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, + CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + }; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::radarr_context_clues::{ - ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, - COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, - MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, + RadarrContextClueProvider, ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, + COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES, }; + use crate::app::App; + use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; + use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; #[test] fn test_library_context_clues() { @@ -179,26 +186,15 @@ mod tests { let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap(); - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); - assert_eq!(manual_movie_search_context_clues_iter.next(), None); - } - - #[test] - fn test_manual_movie_search_contextual_context_clues() { - let mut manual_movie_search_contextual_context_clues_iter = - MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES.iter(); - - let (key_binding, description) = manual_movie_search_contextual_context_clues_iter - .next() - .unwrap(); - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); assert_str_eq!(*description, "details"); - assert_eq!( - manual_movie_search_contextual_context_clues_iter.next(), - None - ); + + let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); + + assert_eq!(manual_movie_search_context_clues_iter.next(), None); } #[test] @@ -254,4 +250,255 @@ mod tests { assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_eq!(collection_details_context_clues_iter.next(), None); } + + #[test] + #[should_panic( + expected = "RadarrContextClueProvider::get_context_clues called with non-Radarr route" + )] + fn test_radarr_context_clue_provider_get_context_clues_non_radarr_route() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveSonarrBlock::default().into()); + + // This should panic because the route is not a Radarr route + RadarrContextClueProvider::get_context_clues(&mut app); + } + + #[rstest] + #[case(ActiveRadarrBlock::TestAllIndexers, None)] + #[case(ActiveRadarrBlock::AddMovieSearchInput, None)] + #[case(ActiveRadarrBlock::AddMovieEmptySearchResults, None)] + #[case(ActiveRadarrBlock::SystemLogs, None)] + #[case(ActiveRadarrBlock::SystemUpdates, None)] + #[case(ActiveRadarrBlock::ViewMovieOverview, None)] + #[case( + ActiveRadarrBlock::CollectionDetails, + Some(ActiveRadarrBlock::ViewMovieOverview) + )] + fn test_radarr_context_clue_provider_bare_popup_context_clues( + #[case] active_radarr_block: ActiveRadarrBlock, + #[case] context_option: Option, + ) { + let mut app = App::test_default(); + app.push_navigation_stack((active_radarr_block, context_option).into()); + + let context_clues = RadarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(&BARE_POPUP_CONTEXT_CLUES, context_clues.unwrap()); + } + + #[rstest] + #[case(0, ActiveRadarrBlock::MovieDetails, &MOVIE_DETAILS_CONTEXT_CLUES)] + #[case(1, ActiveRadarrBlock::MovieHistory, &MOVIE_DETAILS_CONTEXT_CLUES)] + #[case(2, ActiveRadarrBlock::FileInfo, &MOVIE_DETAILS_CONTEXT_CLUES)] + #[case(3, ActiveRadarrBlock::Cast, &MOVIE_DETAILS_CONTEXT_CLUES)] + #[case(4, ActiveRadarrBlock::Crew, &MOVIE_DETAILS_CONTEXT_CLUES)] + #[case(5, ActiveRadarrBlock::ManualSearch, &MANUAL_MOVIE_SEARCH_CONTEXT_CLUES)] + fn test_radarr_context_clue_provider_movie_details_block_context_clues( + #[case] index: usize, + #[case] active_radarr_block: ActiveRadarrBlock, + #[case] expected_context_clues: &[ContextClue], + ) { + let mut app = App::test_default(); + app.data.radarr_data = RadarrData::default(); + app.data.radarr_data.movie_info_tabs.set_index(index); + app.push_navigation_stack(active_radarr_block.into()); + + let context_clues = RadarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(expected_context_clues, context_clues.unwrap()); + } + + #[rstest] + fn test_radarr_context_clue_provider_confirmation_prompt_context_clues( + #[values( + ActiveRadarrBlock::AddMoviePrompt, + ActiveRadarrBlock::AddMovieSelectMonitor, + ActiveRadarrBlock::AddMovieSelectMinimumAvailability, + ActiveRadarrBlock::AddMovieSelectQualityProfile, + ActiveRadarrBlock::AddMovieSelectRootFolder, + ActiveRadarrBlock::AddMovieTagsInput, + ActiveRadarrBlock::SystemTaskStartConfirmPrompt + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::test_default(); + app.data.radarr_data = RadarrData::default(); + app.push_navigation_stack(active_radarr_block.into()); + + let context_clues = RadarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(&CONFIRMATION_PROMPT_CONTEXT_CLUES, context_clues.unwrap()); + } + + #[rstest] + fn test_radarr_context_clue_provider_confirmation_prompt_context_clues_edit_collection_blocks( + #[values( + ActiveRadarrBlock::EditCollectionPrompt, + ActiveRadarrBlock::EditCollectionConfirmPrompt, + ActiveRadarrBlock::EditCollectionRootFolderPathInput, + ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, + ActiveRadarrBlock::EditCollectionSelectQualityProfile, + ActiveRadarrBlock::EditCollectionToggleSearchOnAdd, + ActiveRadarrBlock::EditCollectionToggleMonitored + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::test_default(); + app.data.radarr_data = RadarrData::default(); + app.push_navigation_stack(active_radarr_block.into()); + + let context_clues = RadarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(&CONFIRMATION_PROMPT_CONTEXT_CLUES, context_clues.unwrap()); + } + + #[rstest] + fn test_radarr_context_clue_provider_confirmation_prompt_context_clues_edit_indexer_blocks( + #[values( + ActiveRadarrBlock::EditIndexerPrompt, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ActiveRadarrBlock::EditIndexerApiKeyInput, + ActiveRadarrBlock::EditIndexerNameInput, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + ActiveRadarrBlock::EditIndexerToggleEnableRss, + ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveRadarrBlock::EditIndexerPriorityInput, + ActiveRadarrBlock::EditIndexerUrlInput, + ActiveRadarrBlock::EditIndexerTagsInput + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::test_default(); + app.data.radarr_data = RadarrData::default(); + app.push_navigation_stack(active_radarr_block.into()); + + let context_clues = RadarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(&CONFIRMATION_PROMPT_CONTEXT_CLUES, context_clues.unwrap()); + } + + #[rstest] + fn test_radarr_context_clue_provider_confirmation_prompt_context_clues_indexer_settings_blocks( + #[values( + ActiveRadarrBlock::AllIndexerSettingsPrompt, + ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, + ActiveRadarrBlock::IndexerSettingsConfirmPrompt, + ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, + ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, + ActiveRadarrBlock::IndexerSettingsRetentionInput, + ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, + ActiveRadarrBlock::IndexerSettingsToggleAllowHardcodedSubs, + ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags, + ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::test_default(); + app.data.radarr_data = RadarrData::default(); + app.push_navigation_stack(active_radarr_block.into()); + + let context_clues = RadarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(&CONFIRMATION_PROMPT_CONTEXT_CLUES, context_clues.unwrap()); + } + + #[rstest] + fn test_radarr_context_clue_provider_confirmation_prompt_context_clues_edit_movie_blocks( + #[values( + ActiveRadarrBlock::EditMoviePrompt, + ActiveRadarrBlock::EditMovieConfirmPrompt, + ActiveRadarrBlock::EditMoviePathInput, + ActiveRadarrBlock::EditMovieSelectMinimumAvailability, + ActiveRadarrBlock::EditMovieSelectQualityProfile, + ActiveRadarrBlock::EditMovieTagsInput, + ActiveRadarrBlock::EditMovieToggleMonitored + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::test_default(); + app.data.radarr_data = RadarrData::default(); + app.push_navigation_stack(active_radarr_block.into()); + + let context_clues = RadarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(&CONFIRMATION_PROMPT_CONTEXT_CLUES, context_clues.unwrap()); + } + + #[rstest] + fn test_radarr_context_clue_provider_add_movie_search_results_context_clues( + #[values( + ActiveRadarrBlock::AddMovieSearchResults, + ActiveRadarrBlock::AddMovieAlreadyInLibrary + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::test_default(); + app.data.radarr_data = RadarrData::default(); + app.push_navigation_stack(active_radarr_block.into()); + + let context_clues = RadarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!( + &ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, + context_clues.unwrap() + ); + } + + #[test] + fn test_radarr_context_clue_provider_collection_details_context_clues() { + let mut app = App::test_default(); + app.data.radarr_data = RadarrData::default(); + app.push_navigation_stack(ActiveRadarrBlock::CollectionDetails.into()); + + let context_clues = RadarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(&COLLECTION_DETAILS_CONTEXT_CLUES, context_clues.unwrap()); + } + + #[test] + fn test_radarr_context_clue_provider_system_tasks_context_clues() { + let mut app = App::test_default(); + app.data.radarr_data = RadarrData::default(); + + app.push_navigation_stack(ActiveRadarrBlock::SystemTasks.into()); + + let context_clues = RadarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(&SYSTEM_TASKS_CONTEXT_CLUES, context_clues.unwrap()); + } + + #[rstest] + #[case(0, ActiveRadarrBlock::Movies, &LIBRARY_CONTEXT_CLUES)] + #[case(1, ActiveRadarrBlock::Collections, &COLLECTIONS_CONTEXT_CLUES)] + #[case(2, ActiveRadarrBlock::Downloads, &DOWNLOADS_CONTEXT_CLUES)] + #[case(3, ActiveRadarrBlock::Blocklist, &BLOCKLIST_CONTEXT_CLUES)] + #[case(4, ActiveRadarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)] + #[case(5, ActiveRadarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)] + #[case(6, ActiveRadarrBlock::System, &SYSTEM_CONTEXT_CLUES)] + fn test_radarr_context_clue_provider_radarr_blocks_context_clues( + #[case] index: usize, + #[case] active_radarr_block: ActiveRadarrBlock, + #[case] expected_context_clues: &[ContextClue], + ) { + let mut app = App::test_default(); + app.data.radarr_data = RadarrData::default(); + app.data.radarr_data.main_tabs.set_index(index); + app.push_navigation_stack(active_radarr_block.into()); + + let context_clues = RadarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(expected_context_clues, context_clues.unwrap()); + } } diff --git a/src/app/sonarr/sonarr_context_clues.rs b/src/app/sonarr/sonarr_context_clues.rs index 138d51d..28b642a 100644 --- a/src/app/sonarr/sonarr_context_clues.rs +++ b/src/app/sonarr/sonarr_context_clues.rs @@ -1,4 +1,12 @@ -use crate::app::{context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS}; +use crate::app::context_clues::{ + ContextClueProvider, BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, +}; +use crate::app::{context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS, App}; +use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, ADD_SERIES_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_SERIES_BLOCKS, + EPISODE_DETAILS_BLOCKS, INDEXER_SETTINGS_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS, +}; +use crate::models::Route; #[cfg(test)] #[path = "sonarr_context_clues_tests.rs"] @@ -79,12 +87,7 @@ pub static SERIES_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [ (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), ]; -pub static SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 2] = [ - (DEFAULT_KEYBINDINGS.submit, "episode details"), - (DEFAULT_KEYBINDINGS.delete, "delete episode"), -]; - -pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [ +pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 7] = [ ( DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh.desc, @@ -99,9 +102,11 @@ pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [ DEFAULT_KEYBINDINGS.auto_search.desc, ), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), + (DEFAULT_KEYBINDINGS.submit, "episode details"), + (DEFAULT_KEYBINDINGS.delete, "delete episode"), ]; -pub static SEASON_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [ +pub static SEASON_HISTORY_CONTEXT_CLUES: [ContextClue; 7] = [ ( DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh.desc, @@ -113,10 +118,11 @@ pub static SEASON_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [ DEFAULT_KEYBINDINGS.auto_search, DEFAULT_KEYBINDINGS.auto_search.desc, ), + (DEFAULT_KEYBINDINGS.submit, "details"), (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), ]; -pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [ +pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [ ( DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh.desc, @@ -126,10 +132,11 @@ pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [ DEFAULT_KEYBINDINGS.auto_search.desc, ), (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.submit, "details"), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; -pub static MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [ +pub static MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 5] = [ ( DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh.desc, @@ -139,12 +146,10 @@ pub static MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [ DEFAULT_KEYBINDINGS.auto_search.desc, ), (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.submit, "details"), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; -pub static DETAILS_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 1] = - [(DEFAULT_KEYBINDINGS.submit, "details")]; - pub static EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [ ( DEFAULT_KEYBINDINGS.refresh, @@ -157,7 +162,84 @@ pub static EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [ (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; +pub static SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 4] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), +]; + pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [ (DEFAULT_KEYBINDINGS.submit, "start task"), (DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), ]; + +pub(in crate::app) struct SonarrContextClueProvider; + +impl ContextClueProvider for SonarrContextClueProvider { + fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> { + let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() else { + panic!("SonarrContextClueProvider::get_context_clues called with non-Sonarr route"); + }; + match active_sonarr_block { + _ if SERIES_DETAILS_BLOCKS.contains(&active_sonarr_block) => app + .data + .sonarr_data + .series_info_tabs + .get_active_route_contextual_help(), + _ if SEASON_DETAILS_BLOCKS.contains(&active_sonarr_block) => app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_details_tabs + .get_active_route_contextual_help(), + _ if EPISODE_DETAILS_BLOCKS.contains(&active_sonarr_block) => app + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_details_tabs + .get_active_route_contextual_help(), + ActiveSonarrBlock::TestAllIndexers + | ActiveSonarrBlock::AddSeriesSearchInput + | ActiveSonarrBlock::AddSeriesEmptySearchResults + | ActiveSonarrBlock::SystemLogs + | ActiveSonarrBlock::SystemUpdates => Some(&BARE_POPUP_CONTEXT_CLUES), + _ if EDIT_INDEXER_BLOCKS.contains(&active_sonarr_block) + || INDEXER_SETTINGS_BLOCKS.contains(&active_sonarr_block) + || EDIT_SERIES_BLOCKS.contains(&active_sonarr_block) => + { + Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES) + } + ActiveSonarrBlock::AddSeriesPrompt + | ActiveSonarrBlock::AddSeriesSelectMonitor + | ActiveSonarrBlock::AddSeriesSelectSeriesType + | ActiveSonarrBlock::AddSeriesSelectQualityProfile + | ActiveSonarrBlock::AddSeriesSelectLanguageProfile + | ActiveSonarrBlock::AddSeriesSelectRootFolder + | ActiveSonarrBlock::AddSeriesTagsInput + | ActiveSonarrBlock::SystemTaskStartConfirmPrompt => Some(&CONFIRMATION_PROMPT_CONTEXT_CLUES), + _ if ADD_SERIES_BLOCKS.contains(&active_sonarr_block) => { + Some(&ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES) + } + ActiveSonarrBlock::SystemTasks => Some(&SYSTEM_TASKS_CONTEXT_CLUES), + _ => app + .data + .sonarr_data + .main_tabs + .get_active_route_contextual_help(), + } + } +} diff --git a/src/app/sonarr/sonarr_context_clues_tests.rs b/src/app/sonarr/sonarr_context_clues_tests.rs index 6be453b..2560111 100644 --- a/src/app/sonarr/sonarr_context_clues_tests.rs +++ b/src/app/sonarr/sonarr_context_clues_tests.rs @@ -1,17 +1,29 @@ #[cfg(test)] mod tests { - use pretty_assertions::{assert_eq, assert_str_eq}; - + use crate::app::context_clues::{ + ContextClue, ContextClueProvider, BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, + CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + }; + use crate::app::sonarr::sonarr_context_clues::{ + SonarrContextClueProvider, SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES, + }; use crate::app::{ key_binding::DEFAULT_KEYBINDINGS, sonarr::sonarr_context_clues::{ - ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, DETAILS_CONTEXTUAL_CONTEXT_CLUES, - EPISODE_DETAILS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, - MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, - SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, - SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES, + ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES, + HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, + MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, + SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, + SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES, }, + App, }; + use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; + use crate::models::servarr_data::sonarr::modals::{EpisodeDetailsModal, SeasonDetailsModal}; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; #[test] fn test_add_series_search_results_context_clues() { @@ -252,23 +264,18 @@ mod tests { assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); - assert_eq!(season_details_context_clues_iter.next(), None); - } - #[test] - fn test_season_details_contextual_context_clues() { - let mut season_details_contextual_context_clues_iter = - SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES.iter(); - let (key_binding, description) = season_details_contextual_context_clues_iter.next().unwrap(); + let (key_binding, description) = season_details_context_clues_iter.next().unwrap(); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); assert_str_eq!(*description, "episode details"); - let (key_binding, description) = season_details_contextual_context_clues_iter.next().unwrap(); + let (key_binding, description) = season_details_context_clues_iter.next().unwrap(); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); assert_str_eq!(*description, "delete episode"); - assert_eq!(season_details_contextual_context_clues_iter.next(), None); + + assert_eq!(season_details_context_clues_iter.next(), None); } #[test] @@ -301,6 +308,11 @@ mod tests { let (key_binding, description) = season_history_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "details"); + + let (key_binding, description) = season_history_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); assert_str_eq!(*description, "cancel filter/close"); assert_eq!(season_history_context_clues_iter.next(), None); @@ -327,6 +339,11 @@ mod tests { let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "details"); + + let (key_binding, description) = manual_season_search_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_eq!(manual_season_search_context_clues_iter.next(), None); @@ -353,21 +370,16 @@ mod tests { let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "details"); + + let (key_binding, description) = manual_episode_search_context_clues_iter.next().unwrap(); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_eq!(manual_episode_search_context_clues_iter.next(), None); } - #[test] - fn details_contextual_context_clues() { - let mut manual_search_contextual_context_clues_iter = DETAILS_CONTEXTUAL_CONTEXT_CLUES.iter(); - let (key_binding, description) = manual_search_contextual_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); - assert_str_eq!(*description, "details"); - assert_eq!(manual_search_contextual_context_clues_iter.next(), None); - } - #[test] fn test_episode_details_context_clues() { let mut episode_details_context_clues_iter = EPISODE_DETAILS_CONTEXT_CLUES.iter(); @@ -389,6 +401,32 @@ mod tests { assert_eq!(episode_details_context_clues_iter.next(), None); } + #[test] + fn test_selectable_episode_details_context_clues() { + let mut episode_details_context_clues_iter = SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES.iter(); + + let (key_binding, description) = episode_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = episode_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc); + + let (key_binding, description) = episode_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "details"); + + let (key_binding, description) = episode_details_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); + assert_eq!(episode_details_context_clues_iter.next(), None); + } + #[test] fn test_system_tasks_context_clues() { let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter(); @@ -404,4 +442,258 @@ mod tests { assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_eq!(system_tasks_context_clues_iter.next(), None); } + + #[test] + #[should_panic( + expected = "SonarrContextClueProvider::get_context_clues called with non-Sonarr route" + )] + fn test_sonarr_context_clue_provider_get_context_clues_non_sonarr_route() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveRadarrBlock::default().into()); + + // This should panic because the route is not a Sonarr route + SonarrContextClueProvider::get_context_clues(&mut app); + } + + #[rstest] + #[case(0, ActiveSonarrBlock::SeriesDetails, &SERIES_DETAILS_CONTEXT_CLUES)] + #[case(1, ActiveSonarrBlock::SeriesHistory, &SERIES_HISTORY_CONTEXT_CLUES)] + fn test_sonarr_context_clue_provider_series_info_tabs( + #[case] index: usize, + #[case] active_sonarr_block: ActiveSonarrBlock, + #[case] expected_context_clues: &[ContextClue], + ) { + let mut app = App::test_default(); + app.data.sonarr_data = SonarrData::default(); + app.data.sonarr_data.series_info_tabs.set_index(index); + app.push_navigation_stack(active_sonarr_block.into()); + + let context_clues = SonarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(expected_context_clues, context_clues.unwrap()); + } + + #[rstest] + #[case(0, ActiveSonarrBlock::SeasonDetails, &SEASON_DETAILS_CONTEXT_CLUES)] + #[case(1, ActiveSonarrBlock::SeasonHistory, &SEASON_HISTORY_CONTEXT_CLUES)] + #[case(2, ActiveSonarrBlock::ManualSeasonSearch, &MANUAL_SEASON_SEARCH_CONTEXT_CLUES)] + fn test_sonarr_context_clue_provider_season_details_tabs( + #[case] index: usize, + #[case] active_sonarr_block: ActiveSonarrBlock, + #[case] expected_context_clues: &[ContextClue], + ) { + let mut app = App::test_default(); + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.season_details_tabs.set_index(index); + let sonarr_data = SonarrData { + season_details_modal: Some(season_details_modal), + ..SonarrData::default() + }; + app.data.sonarr_data = sonarr_data; + app.push_navigation_stack(active_sonarr_block.into()); + + let context_clues = SonarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(expected_context_clues, context_clues.unwrap()); + } + + #[rstest] + #[case(0, ActiveSonarrBlock::EpisodeDetails, &EPISODE_DETAILS_CONTEXT_CLUES)] + #[case(1, ActiveSonarrBlock::EpisodeHistory, &SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES)] + #[case(2, ActiveSonarrBlock::EpisodeFile, &EPISODE_DETAILS_CONTEXT_CLUES)] + #[case(3, ActiveSonarrBlock::ManualEpisodeSearch, &MANUAL_EPISODE_SEARCH_CONTEXT_CLUES)] + fn test_sonarr_context_clue_provider_episode_details_tabs( + #[case] index: usize, + #[case] active_sonarr_block: ActiveSonarrBlock, + #[case] expected_context_clues: &[ContextClue], + ) { + let mut app = App::test_default(); + let mut episode_details_modal = EpisodeDetailsModal::default(); + episode_details_modal.episode_details_tabs.set_index(index); + let sonarr_data = SonarrData { + season_details_modal: Some(SeasonDetailsModal { + episode_details_modal: Some(episode_details_modal), + ..SeasonDetailsModal::default() + }), + ..SonarrData::default() + }; + app.data.sonarr_data = sonarr_data; + app.push_navigation_stack(active_sonarr_block.into()); + + let context_clues = SonarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(expected_context_clues, context_clues.unwrap()); + } + + #[rstest] + fn test_sonarr_context_clue_provider_bare_popup_context_clues( + #[values( + ActiveSonarrBlock::TestAllIndexers, + ActiveSonarrBlock::AddSeriesSearchInput, + ActiveSonarrBlock::AddSeriesEmptySearchResults, + ActiveSonarrBlock::SystemLogs, + ActiveSonarrBlock::SystemUpdates + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_sonarr_block.into()); + + let context_clues = SonarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(context_clues.unwrap(), &BARE_POPUP_CONTEXT_CLUES); + } + + #[rstest] + fn test_sonarr_context_clue_provider_confirmation_prompt_context_clues( + #[values( + ActiveSonarrBlock::AddSeriesPrompt, + ActiveSonarrBlock::AddSeriesSelectMonitor, + ActiveSonarrBlock::AddSeriesSelectSeriesType, + ActiveSonarrBlock::AddSeriesSelectQualityProfile, + ActiveSonarrBlock::AddSeriesSelectLanguageProfile, + ActiveSonarrBlock::AddSeriesSelectRootFolder, + ActiveSonarrBlock::AddSeriesTagsInput, + ActiveSonarrBlock::SystemTaskStartConfirmPrompt + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_sonarr_block.into()); + + let context_clues = SonarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(context_clues.unwrap(), &CONFIRMATION_PROMPT_CONTEXT_CLUES); + } + + #[rstest] + fn test_sonarr_context_clue_provider_confirmation_prompt_popup_clues_edit_indexer_blocks( + #[values( + ActiveSonarrBlock::EditIndexerPrompt, + ActiveSonarrBlock::EditIndexerConfirmPrompt, + ActiveSonarrBlock::EditIndexerApiKeyInput, + ActiveSonarrBlock::EditIndexerNameInput, + ActiveSonarrBlock::EditIndexerSeedRatioInput, + ActiveSonarrBlock::EditIndexerToggleEnableRss, + ActiveSonarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveSonarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveSonarrBlock::EditIndexerPriorityInput, + ActiveSonarrBlock::EditIndexerUrlInput, + ActiveSonarrBlock::EditIndexerTagsInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_sonarr_block.into()); + + let context_clues = SonarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(context_clues.unwrap(), &CONFIRMATION_PROMPT_CONTEXT_CLUES); + } + + #[rstest] + fn test_sonarr_context_clue_provider_confirmation_prompt_popup_clues_indexer_settings_blocks( + #[values( + ActiveSonarrBlock::AllIndexerSettingsPrompt, + ActiveSonarrBlock::IndexerSettingsConfirmPrompt, + ActiveSonarrBlock::IndexerSettingsMaximumSizeInput, + ActiveSonarrBlock::IndexerSettingsMinimumAgeInput, + ActiveSonarrBlock::IndexerSettingsRetentionInput, + ActiveSonarrBlock::IndexerSettingsRssSyncIntervalInput + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_sonarr_block.into()); + + let context_clues = SonarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(context_clues.unwrap(), &CONFIRMATION_PROMPT_CONTEXT_CLUES); + } + + #[rstest] + fn test_sonarr_context_clue_provider_confirmation_prompt_popup_clues_edit_series_blocks( + #[values( + ActiveSonarrBlock::EditSeriesPrompt, + ActiveSonarrBlock::EditSeriesConfirmPrompt, + ActiveSonarrBlock::EditSeriesPathInput, + ActiveSonarrBlock::EditSeriesSelectSeriesType, + ActiveSonarrBlock::EditSeriesSelectQualityProfile, + ActiveSonarrBlock::EditSeriesSelectLanguageProfile, + ActiveSonarrBlock::EditSeriesTagsInput, + ActiveSonarrBlock::EditSeriesToggleMonitored, + ActiveSonarrBlock::EditSeriesToggleSeasonFolder + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_sonarr_block.into()); + + let context_clues = SonarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(context_clues.unwrap(), &CONFIRMATION_PROMPT_CONTEXT_CLUES); + } + + #[rstest] + fn test_sonarr_context_clue_provider_add_series_search_results_clues( + #[values( + ActiveSonarrBlock::AddSeriesAlreadyInLibrary, + ActiveSonarrBlock::AddSeriesSearchResults + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(active_sonarr_block.into()); + + let context_clues = SonarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!( + context_clues.unwrap(), + &ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES + ); + } + + #[test] + fn test_sonarr_context_clue_provider_system_tasks_clues() { + let mut app = App::test_default(); + + app.push_navigation_stack(ActiveSonarrBlock::SystemTasks.into()); + let context_clues = SonarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(context_clues.unwrap(), &SYSTEM_TASKS_CONTEXT_CLUES); + } + + #[rstest] + #[case(0, ActiveSonarrBlock::Series, &SERIES_CONTEXT_CLUES)] + #[case(1, ActiveSonarrBlock::Downloads, &DOWNLOADS_CONTEXT_CLUES)] + #[case(2, ActiveSonarrBlock::Blocklist, &BLOCKLIST_CONTEXT_CLUES)] + #[case(3, ActiveSonarrBlock::History, &HISTORY_CONTEXT_CLUES)] + #[case(4, ActiveSonarrBlock::RootFolders, &ROOT_FOLDERS_CONTEXT_CLUES)] + #[case(5, ActiveSonarrBlock::Indexers, &INDEXERS_CONTEXT_CLUES)] + #[case(6, ActiveSonarrBlock::System, &SYSTEM_CONTEXT_CLUES)] + fn test_sonarr_context_clue_provider_sonarr_tabs( + #[case] index: usize, + #[case] active_sonarr_block: ActiveSonarrBlock, + #[case] expected_context_clues: &[ContextClue], + ) { + let mut app = App::test_default(); + app.data.sonarr_data = SonarrData::default(); + app.data.sonarr_data.main_tabs.set_index(index); + app.push_navigation_stack(active_sonarr_block.into()); + + let context_clues = SonarrContextClueProvider::get_context_clues(&mut app); + + assert!(context_clues.is_some()); + assert_eq!(expected_context_clues, context_clues.unwrap()); + } } diff --git a/src/event/key.rs b/src/event/key.rs index 456ba83..f0146f7 100644 --- a/src/event/key.rs +++ b/src/event/key.rs @@ -31,23 +31,23 @@ pub enum Key { impl Display for Key { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match *self { - Key::Char(c) => write!(f, "<{c}>"), - Key::Ctrl(c) => write!(f, ""), - Key::Up => write!(f, "<↑>"), - Key::Down => write!(f, "<↓>"), - Key::Left => write!(f, "<←>"), - Key::Right => write!(f, "<→>"), - Key::PgDown => write!(f, ""), - Key::PgUp => write!(f, ""), - Key::Enter => write!(f, ""), - Key::Esc => write!(f, ""), - Key::Backspace => write!(f, ""), - Key::Home => write!(f, ""), - Key::End => write!(f, ""), - Key::Tab => write!(f, ""), - Key::BackTab => write!(f, ""), - Key::Delete => write!(f, ""), - _ => write!(f, "<{self:?}>"), + Key::Char(c) => write!(f, "{c}"), + Key::Ctrl(c) => write!(f, "ctrl-{c}"), + Key::Up => write!(f, "↑"), + Key::Down => write!(f, "↓"), + Key::Left => write!(f, "←"), + Key::Right => write!(f, "→"), + Key::PgDown => write!(f, "pgDown"), + Key::PgUp => write!(f, "pgUp"), + Key::Enter => write!(f, "enter"), + Key::Esc => write!(f, "esc"), + Key::Backspace => write!(f, "backspace"), + Key::Home => write!(f, "home"), + Key::End => write!(f, "end"), + Key::Tab => write!(f, "tab"), + Key::BackTab => write!(f, "shift-tab"), + Key::Delete => write!(f, "del"), + _ => write!(f, "{self:?}"), } } } diff --git a/src/event/key_tests.rs b/src/event/key_tests.rs index 85ab6f3..8ef6d4c 100644 --- a/src/event/key_tests.rs +++ b/src/event/key_tests.rs @@ -11,8 +11,8 @@ mod tests { #[case(Key::Down, "↓")] #[case(Key::Left, "←")] #[case(Key::Right, "→")] - #[case(Key::PgDown, "C-d")] - #[case(Key::PgUp, "C-u")] + #[case(Key::PgDown, "pgDown")] + #[case(Key::PgUp, "pgUp")] #[case(Key::Enter, "enter")] #[case(Key::Esc, "esc")] #[case(Key::Backspace, "backspace")] @@ -22,9 +22,9 @@ mod tests { #[case(Key::BackTab, "shift-tab")] #[case(Key::Delete, "del")] #[case(Key::Char('q'), "q")] - #[case(Key::Ctrl('q'), "C-q")] + #[case(Key::Ctrl('q'), "ctrl-q")] fn test_key_formatter(#[case] key: Key, #[case] expected_str: &str) { - assert_str_eq!(format!("{key}"), format!("<{expected_str}>")); + assert_str_eq!(format!("{key}"), format!("{expected_str}")); } #[test] diff --git a/src/handlers/handlers_tests.rs b/src/handlers/handlers_tests.rs index c4c9977..ac81556 100644 --- a/src/handlers/handlers_tests.rs +++ b/src/handlers/handlers_tests.rs @@ -6,13 +6,20 @@ mod tests { use rstest::rstest; use tokio_util::sync::CancellationToken; - use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::context_clues::SERVARR_CONTEXT_CLUES; + use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS}; + use crate::app::radarr::radarr_context_clues::{ + LIBRARY_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, + }; use crate::app::App; use crate::event::Key; - use crate::handlers::handle_events; use crate::handlers::{handle_clear_errors, handle_prompt_toggle}; - use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; + use crate::handlers::{handle_events, populate_keymapping_table}; + use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; + use crate::models::servarr_data::ActiveKeybindingBlock; + use crate::models::servarr_models::KeybindingItem; + use crate::models::stateful_table::StatefulTable; use crate::models::HorizontallyScrollableText; use crate::models::Route; @@ -82,6 +89,82 @@ mod tests { assert!(app.cancellation_token.is_cancelled()); } + #[test] + fn test_handle_populate_keybindings_table_on_help_button_press() { + let mut app = App::test_default(); + let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES) + .iter() + .map(|(key, desc)| context_clue_to_keybinding_item(key, desc)) + .collect::>(); + app.push_navigation_stack(ActiveKeybindingBlock::Help.into()); + + handle_events(DEFAULT_KEYBINDINGS.help.key, &mut app); + + assert!(app.keymapping_table.is_some()); + assert_eq!( + expected_keybinding_items, + app.keymapping_table.unwrap().items + ); + } + + #[test] + fn test_handle_ignore_help_button_when_ignore_special_keys_for_textbox_input_is_true() { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = true; + app.push_navigation_stack(ActiveRadarrBlock::default().into()); + + handle_events(DEFAULT_KEYBINDINGS.help.key, &mut app); + + assert!(app.keymapping_table.is_none()); + } + + #[test] + fn test_handle_empties_keybindings_table_on_help_button_press_when_keybindings_table_is_already_populated( + ) { + let mut app = App::test_default(); + let keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES) + .iter() + .map(|(key, desc)| context_clue_to_keybinding_item(key, desc)) + .collect::>(); + let mut stateful_table = StatefulTable::default(); + stateful_table.set_items(keybinding_items); + app.keymapping_table = Some(stateful_table); + app.push_navigation_stack(ActiveRadarrBlock::default().into()); + + handle_events(DEFAULT_KEYBINDINGS.help.key, &mut app); + + assert!(app.keymapping_table.is_none()); + } + + #[test] + fn test_handle_shows_keymapping_popup_when_keymapping_table_is_populated() { + let mut app = App::test_default(); + let keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES) + .iter() + .map(|(key, desc)| context_clue_to_keybinding_item(key, desc)) + .collect::>(); + let mut stateful_table = StatefulTable::default(); + stateful_table.set_items(keybinding_items); + app.keymapping_table = Some(stateful_table); + app.push_navigation_stack(ActiveRadarrBlock::default().into()); + let expected_selection = KeybindingItem { + key: SERVARR_CONTEXT_CLUES[1].0.key.to_string(), + alt_key: SERVARR_CONTEXT_CLUES[1] + .0 + .alt + .map_or(String::new(), |k| k.to_string()), + desc: SERVARR_CONTEXT_CLUES[1].1.to_string(), + }; + + handle_events(DEFAULT_KEYBINDINGS.down.key, &mut app); + + assert!(app.keymapping_table.is_some()); + assert_eq!( + &expected_selection, + app.keymapping_table.unwrap().current_selection() + ); + } + #[rstest] fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) { let mut app = App::test_default(); @@ -113,4 +196,95 @@ mod tests { assert!(!app.data.sonarr_data.prompt_confirm); } + + #[test] + fn test_populate_keymapping_table_global_options() { + let expected_keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES) + .iter() + .map(|(key, desc)| { + let (key, alt_key) = if key.alt.is_some() { + (key.key.to_string(), key.alt.as_ref().unwrap().to_string()) + } else { + (key.key.to_string(), String::new()) + }; + KeybindingItem { + key, + alt_key, + desc: desc.to_string(), + } + }) + .collect::>(); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveKeybindingBlock::Help.into()); + + populate_keymapping_table(&mut app); + + assert!(app.keymapping_table.is_some()); + assert_eq!( + expected_keybinding_items, + app.keymapping_table.unwrap().items + ); + } + + #[test] + fn test_populate_keymapping_table_populates_servarr_specific_tab_info_before_global_options() { + let mut expected_keybinding_items = LIBRARY_CONTEXT_CLUES + .iter() + .map(|(key, desc)| context_clue_to_keybinding_item(key, desc)) + .collect::>(); + expected_keybinding_items.extend( + SERVARR_CONTEXT_CLUES + .iter() + .map(|(key, desc)| context_clue_to_keybinding_item(key, desc)), + ); + let mut app = App::test_default(); + app.data.radarr_data = RadarrData::default(); + app.push_navigation_stack(ActiveRadarrBlock::default().into()); + + populate_keymapping_table(&mut app); + + assert!(app.keymapping_table.is_some()); + assert_eq!( + expected_keybinding_items, + app.keymapping_table.unwrap().items + ); + } + + #[test] + fn test_populate_keymapping_table_populates_delegated_servarr_context_provider_options_before_global_options( + ) { + let mut expected_keybinding_items = MOVIE_DETAILS_CONTEXT_CLUES + .iter() + .map(|(key, desc)| context_clue_to_keybinding_item(key, desc)) + .collect::>(); + expected_keybinding_items.extend( + SERVARR_CONTEXT_CLUES + .iter() + .map(|(key, desc)| context_clue_to_keybinding_item(key, desc)), + ); + let mut app = App::test_default(); + app.data.radarr_data = RadarrData::default(); + app.push_navigation_stack(ActiveRadarrBlock::MovieDetails.into()); + + populate_keymapping_table(&mut app); + + assert!(app.keymapping_table.is_some()); + assert_eq!( + expected_keybinding_items, + app.keymapping_table.unwrap().items + ); + } + + fn context_clue_to_keybinding_item(key: &KeyBinding, desc: &&str) -> KeybindingItem { + let (key, alt_key) = if key.alt.is_some() { + (key.key.to_string(), key.alt.as_ref().unwrap().to_string()) + } else { + (key.key.to_string(), String::new()) + }; + KeybindingItem { + key, + alt_key, + desc: desc.to_string(), + } + } } diff --git a/src/handlers/keybinding_handler.rs b/src/handlers/keybinding_handler.rs new file mode 100644 index 0000000..b33dfbd --- /dev/null +++ b/src/handlers/keybinding_handler.rs @@ -0,0 +1,80 @@ +use crate::app::App; +use crate::event::Key; +use crate::handle_table_events; +use crate::handlers::table_handler::TableHandlingConfig; +use crate::handlers::KeyEventHandler; +use crate::models::servarr_data::ActiveKeybindingBlock; +use crate::models::servarr_models::KeybindingItem; + +#[cfg(test)] +#[path = "keybinding_handler_tests.rs"] +mod keybinding_handler_tests; + +pub(super) struct KeybindingHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, +} + +impl KeybindingHandler<'_, '_> { + handle_table_events!( + self, + keybindings, + self.app.keymapping_table.as_mut().unwrap(), + KeybindingItem + ); +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveKeybindingBlock> for KeybindingHandler<'a, 'b> { + fn handle(&mut self) { + let keybinding_table_handling_config = TableHandlingConfig::new(self.app.get_current_route()); + + if !self.handle_keybindings_table_events(keybinding_table_handling_config) { + self.handle_key_event(); + } + } + + fn accepts(_active_block: ActiveKeybindingBlock) -> bool { + true + } + + fn new( + key: Key, + app: &'a mut App<'b>, + _active_block: ActiveKeybindingBlock, + _context: Option, + ) -> KeybindingHandler<'a, 'b> { + KeybindingHandler { key, app } + } + + fn get_key(&self) -> Key { + self.key + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn is_ready(&self) -> bool { + self.app.keymapping_table.is_some() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) {} + + fn handle_submit(&mut self) {} + + fn handle_esc(&mut self) { + self.app.keymapping_table = None; + } + + fn handle_char_key_event(&mut self) {} +} diff --git a/src/handlers/keybinding_handler_tests.rs b/src/handlers/keybinding_handler_tests.rs new file mode 100644 index 0000000..5a44ea8 --- /dev/null +++ b/src/handlers/keybinding_handler_tests.rs @@ -0,0 +1,69 @@ +#[cfg(test)] +mod tests { + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::KeybindingHandler; + use crate::models::servarr_data::ActiveKeybindingBlock; + use crate::models::stateful_table::StatefulTable; + use rstest::rstest; + + mod test_handle_esc { + use super::*; + use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; + use pretty_assertions::assert_eq; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_esc_empties_keymapping_table() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + + KeybindingHandler::new(ESC_KEY, &mut app, ActiveKeybindingBlock::Help, None).handle(); + + assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into()); + assert!(app.keymapping_table.is_none()); + } + } + + #[test] + fn test_keybinding_handler_accepts() { + assert!(KeybindingHandler::accepts(ActiveKeybindingBlock::Help)); + } + + #[test] + fn test_keybinding_handler_not_ready_when_keybinding_is_empty() { + let mut app = App::test_default(); + app.is_loading = false; + + let handler = KeybindingHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveKeybindingBlock::Help, + None, + ); + + assert!(!handler.is_ready()); + } + + #[rstest] + fn test_keybinding_handler_ready_when_keymapping_table_is_not_empty( + #[values(true, false)] is_loading: bool, + ) { + let mut app = App::test_default(); + app.keymapping_table = Some(StatefulTable::default()); + app.is_loading = is_loading; + + let handler = KeybindingHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveKeybindingBlock::Help, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index aea66f5..619cd72 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,11 +1,20 @@ use radarr_handlers::RadarrHandler; use sonarr_handlers::SonarrHandler; +use crate::app::context_clues::{ + ContextClueProvider, ServarrContextClueProvider, SERVARR_CONTEXT_CLUES, +}; +use crate::app::key_binding::KeyBinding; use crate::app::App; use crate::event::Key; +use crate::handlers::keybinding_handler::KeybindingHandler; use crate::matches_key; +use crate::models::servarr_data::ActiveKeybindingBlock; +use crate::models::servarr_models::KeybindingItem; +use crate::models::stateful_table::StatefulTable; use crate::models::{HorizontallyScrollableText, Route}; +mod keybinding_handler; mod radarr_handlers; mod sonarr_handlers; @@ -97,8 +106,17 @@ pub fn handle_events(key: Key, app: &mut App<'_>) { app.server_tabs.previous(); app.pop_and_push_navigation_stack(app.server_tabs.get_active_route()); app.cancellation_token.cancel(); + } else if matches_key!(help, key) && !app.ignore_special_keys_for_textbox_input { + if app.keymapping_table.is_none() { + populate_keymapping_table(app); + } else { + app.keymapping_table = None; + } } else { match app.get_current_route() { + _ if app.keymapping_table.is_some() => { + KeybindingHandler::new(key, app, ActiveKeybindingBlock::Help, None).handle(); + } Route::Radarr(active_radarr_block, context) => { RadarrHandler::new(key, app, active_radarr_block, context).handle() } @@ -110,6 +128,48 @@ pub fn handle_events(key: Key, app: &mut App<'_>) { } } +fn populate_keymapping_table(app: &mut App<'_>) { + let context_clue_to_keybinding_item = |key: &KeyBinding, desc: &&str| { + let (key, alt_key) = if key.alt.is_some() { + (key.key.to_string(), key.alt.as_ref().unwrap().to_string()) + } else { + (key.key.to_string(), String::new()) + }; + KeybindingItem { + key, + alt_key, + desc: desc.to_string(), + } + }; + let mut keybindings = Vec::new(); + let global_keybindings = Vec::from(SERVARR_CONTEXT_CLUES) + .iter() + .map(|(key, desc)| context_clue_to_keybinding_item(key, desc)) + .collect::>(); + + if let Some(contextual_help) = app.server_tabs.get_active_route_contextual_help() { + keybindings.extend( + contextual_help + .iter() + .map(|(key, desc)| context_clue_to_keybinding_item(key, desc)), + ); + } + + if let Some(contextual_help) = ServarrContextClueProvider::get_context_clues(app) { + keybindings.extend( + contextual_help + .iter() + .map(|(key, desc)| context_clue_to_keybinding_item(key, desc)), + ); + } + + keybindings.extend(global_keybindings); + + let mut table = StatefulTable::default(); + table.set_items(keybindings); + app.keymapping_table = Some(table); +} + fn handle_clear_errors(app: &mut App<'_>) { if !app.error.text.is_empty() { app.error = HorizontallyScrollableText::default(); diff --git a/src/models/mod.rs b/src/models/mod.rs index 4d57329..ce52d16 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,6 +1,7 @@ use std::fmt::{Debug, Display, Formatter}; use std::sync::atomic::{AtomicUsize, Ordering}; +use crate::app::context_clues::ContextClue; use crate::app::ServarrConfig; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use radarr_models::RadarrSerdeable; @@ -9,6 +10,7 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Number; use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use sonarr_models::SonarrSerdeable; + pub mod radarr_models; pub mod servarr_data; pub mod servarr_models; @@ -33,6 +35,7 @@ pub enum Route { Bazarr, Prowlarr, Tautulli, + Keybindings, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] @@ -271,8 +274,7 @@ impl HorizontallyScrollableText { pub struct TabRoute { pub title: String, pub route: Route, - pub help: String, - pub contextual_help: Option, + pub contextual_help: Option<&'static [ContextClue]>, pub config: Option, } @@ -286,7 +288,7 @@ impl TabState { TabState { tabs, index: 0 } } - // Allowing this code for now since we'll eventually be implementing additional Servarr support and we'll need it then + // Allowing this code for now since we'll eventually be implementing additional Servarr support, and we'll need it then #[allow(dead_code)] pub fn set_index(&mut self, index: usize) -> &TabRoute { self.index = index; @@ -337,12 +339,8 @@ impl TabState { false } - pub fn get_active_tab_help(&self) -> &str { - &self.tabs[self.index].help - } - - pub fn get_active_tab_contextual_help(&self) -> Option { - self.tabs[self.index].contextual_help.clone() + pub fn get_active_route_contextual_help(&self) -> Option<&'static [ContextClue]> { + self.tabs[self.index].contextual_help } pub fn next(&mut self) { diff --git a/src/models/model_tests.rs b/src/models/model_tests.rs index 38c9cb4..5f3494d 100644 --- a/src/models/model_tests.rs +++ b/src/models/model_tests.rs @@ -3,6 +3,8 @@ mod tests { use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; + use crate::app::context_clues::ContextClue; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::ServarrConfig; use crate::models::from_f64; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; @@ -18,6 +20,10 @@ mod tests { use serde_json::to_string; const BLOCKS: &[&[i32]] = &[&[11, 12], &[21, 22], &[31, 32]]; + static HELP_KEYBINDINGS: [ContextClue; 1] = + [(DEFAULT_KEYBINDINGS.help, DEFAULT_KEYBINDINGS.help.desc)]; + static ESC_KEYBINDINGS: [ContextClue; 1] = + [(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc)]; #[test] fn test_scrollable_text_with_string() { @@ -617,26 +623,15 @@ mod tests { } #[test] - fn test_tab_state_get_active_tab_help() { + fn test_tab_state_get_active_route_contextual_help() { let tabs = create_test_tab_routes(); - let second_tab_help = tabs[1].help.clone(); + let second_tab_help = tabs[1].contextual_help; let tab_state = TabState { tabs, index: 1 }; - let tab_help = tab_state.get_active_tab_help(); + let tab_help = tab_state.get_active_route_contextual_help(); - assert_str_eq!(tab_help, second_tab_help); - } - - #[test] - fn test_tab_state_get_active_tab_contextual_help() { - let tabs = create_test_tab_routes(); - let second_tab_contextual_help = tabs[1].contextual_help.clone().unwrap(); - let tab_state = TabState { tabs, index: 1 }; - - let tab_contextual_help = tab_state.get_active_tab_contextual_help(); - - assert!(tab_contextual_help.is_some()); - assert_str_eq!(tab_contextual_help.unwrap(), second_tab_contextual_help); + assert!(tab_help.is_some()); + assert_eq!(tab_help.unwrap(), second_tab_help.unwrap()); } #[test] @@ -823,15 +818,13 @@ mod tests { TabRoute { title: "Test 1".to_owned(), route: ActiveRadarrBlock::Movies.into(), - help: "Help for Test 1".to_owned(), - contextual_help: Some("Contextual Help for Test 1".to_owned()), + contextual_help: Some(&HELP_KEYBINDINGS), config: None, }, TabRoute { title: "Test 2".to_owned(), route: ActiveRadarrBlock::Collections.into(), - help: "Help for Test 2".to_owned(), - contextual_help: Some("Contextual Help for Test 2".to_owned()), + contextual_help: Some(&ESC_KEYBINDINGS), config: None, }, ] diff --git a/src/models/servarr_data/mod.rs b/src/models/servarr_data/mod.rs index c82e844..dcffb96 100644 --- a/src/models/servarr_data/mod.rs +++ b/src/models/servarr_data/mod.rs @@ -1,3 +1,21 @@ +use crate::models::Route; + pub mod modals; pub mod radarr; pub mod sonarr; + +#[cfg(test)] +#[path = "servarr_data_tests.rs"] +mod servarr_data_tests; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +pub enum ActiveKeybindingBlock { + #[default] + Help, +} + +impl From for Route { + fn from(_active_keybinding_block: ActiveKeybindingBlock) -> Route { + Route::Keybindings + } +} diff --git a/src/models/servarr_data/radarr/radarr_data.rs b/src/models/servarr_data/radarr/radarr_data.rs index fb6326e..1c43015 100644 --- a/src/models/servarr_data/radarr/radarr_data.rs +++ b/src/models/servarr_data/radarr/radarr_data.rs @@ -1,10 +1,10 @@ use crate::app::context_clues::{ - build_context_clue_string, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, - INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }; use crate::app::radarr::radarr_context_clues::{ - COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, - MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, + COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, + MOVIE_DETAILS_CONTEXT_CLUES, }; use crate::models::radarr_models::{ AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DownloadRecord, @@ -123,50 +123,43 @@ impl<'a> Default for RadarrData<'a> { TabRoute { title: "Library".to_string(), route: ActiveRadarrBlock::Movies.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES)), + contextual_help: Some(&LIBRARY_CONTEXT_CLUES), config: None, }, TabRoute { title: "Collections".to_string(), route: ActiveRadarrBlock::Collections.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)), + contextual_help: Some(&COLLECTIONS_CONTEXT_CLUES), config: None, }, TabRoute { title: "Downloads".to_string(), route: ActiveRadarrBlock::Downloads.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)), + contextual_help: Some(&DOWNLOADS_CONTEXT_CLUES), config: None, }, TabRoute { title: "Blocklist".to_string(), route: ActiveRadarrBlock::Blocklist.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)), + contextual_help: Some(&BLOCKLIST_CONTEXT_CLUES), config: None, }, TabRoute { title: "Root Folders".to_string(), route: ActiveRadarrBlock::RootFolders.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)), + contextual_help: Some(&ROOT_FOLDERS_CONTEXT_CLUES), config: None, }, TabRoute { title: "Indexers".to_string(), route: ActiveRadarrBlock::Indexers.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)), + contextual_help: Some(&INDEXERS_CONTEXT_CLUES), config: None, }, TabRoute { title: "System".to_string(), route: ActiveRadarrBlock::System.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)), + contextual_help: Some(&SYSTEM_CONTEXT_CLUES), config: None, }, ]), @@ -174,45 +167,37 @@ impl<'a> Default for RadarrData<'a> { TabRoute { title: "Details".to_string(), route: ActiveRadarrBlock::MovieDetails.into(), - help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), - contextual_help: None, + contextual_help: Some(&MOVIE_DETAILS_CONTEXT_CLUES), config: None, }, TabRoute { title: "History".to_string(), route: ActiveRadarrBlock::MovieHistory.into(), - help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), - contextual_help: None, + contextual_help: Some(&MOVIE_DETAILS_CONTEXT_CLUES), config: None, }, TabRoute { title: "File".to_string(), route: ActiveRadarrBlock::FileInfo.into(), - help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), - contextual_help: None, + contextual_help: Some(&MOVIE_DETAILS_CONTEXT_CLUES), config: None, }, TabRoute { title: "Cast".to_string(), route: ActiveRadarrBlock::Cast.into(), - help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), - contextual_help: None, + contextual_help: Some(&MOVIE_DETAILS_CONTEXT_CLUES), config: None, }, TabRoute { title: "Crew".to_string(), route: ActiveRadarrBlock::Crew.into(), - help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), - contextual_help: None, + contextual_help: Some(&MOVIE_DETAILS_CONTEXT_CLUES), config: None, }, TabRoute { title: "Manual Search".to_string(), route: ActiveRadarrBlock::ManualSearch.into(), - help: build_context_clue_string(&MANUAL_MOVIE_SEARCH_CONTEXT_CLUES), - contextual_help: Some(build_context_clue_string( - &MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, - )), + contextual_help: Some(&MANUAL_MOVIE_SEARCH_CONTEXT_CLUES), config: None, }, ]), diff --git a/src/models/servarr_data/radarr/radarr_data_tests.rs b/src/models/servarr_data/radarr/radarr_data_tests.rs index 7ff699e..cd2d67f 100644 --- a/src/models/servarr_data/radarr/radarr_data_tests.rs +++ b/src/models/servarr_data/radarr/radarr_data_tests.rs @@ -5,12 +5,11 @@ mod tests { use pretty_assertions::{assert_eq, assert_str_eq}; use crate::app::context_clues::{ - build_context_clue_string, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, - INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }; use crate::app::radarr::radarr_context_clues::{ - COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, - MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, + COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, }; @@ -107,10 +106,10 @@ mod tests { radarr_data.main_tabs.tabs[0].route, ActiveRadarrBlock::Movies.into() ); - assert!(radarr_data.main_tabs.tabs[0].help.is_empty()); + assert!(radarr_data.main_tabs.tabs[0].contextual_help.is_some()); assert_eq!( - radarr_data.main_tabs.tabs[0].contextual_help, - Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES)) + radarr_data.main_tabs.tabs[0].contextual_help.unwrap(), + &LIBRARY_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[0].config, None); @@ -119,10 +118,10 @@ mod tests { radarr_data.main_tabs.tabs[1].route, ActiveRadarrBlock::Collections.into() ); - assert!(radarr_data.main_tabs.tabs[1].help.is_empty()); + assert!(radarr_data.main_tabs.tabs[1].contextual_help.is_some()); assert_eq!( - radarr_data.main_tabs.tabs[1].contextual_help, - Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)) + radarr_data.main_tabs.tabs[1].contextual_help.unwrap(), + &COLLECTIONS_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[1].config, None); @@ -131,10 +130,10 @@ mod tests { radarr_data.main_tabs.tabs[2].route, ActiveRadarrBlock::Downloads.into() ); - assert!(radarr_data.main_tabs.tabs[2].help.is_empty()); + assert!(radarr_data.main_tabs.tabs[2].contextual_help.is_some()); assert_eq!( - radarr_data.main_tabs.tabs[2].contextual_help, - Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)) + radarr_data.main_tabs.tabs[2].contextual_help.unwrap(), + &DOWNLOADS_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[2].config, None); @@ -143,10 +142,10 @@ mod tests { radarr_data.main_tabs.tabs[3].route, ActiveRadarrBlock::Blocklist.into() ); - assert!(radarr_data.main_tabs.tabs[3].help.is_empty()); + assert!(radarr_data.main_tabs.tabs[3].contextual_help.is_some()); assert_eq!( - radarr_data.main_tabs.tabs[3].contextual_help, - Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)) + radarr_data.main_tabs.tabs[3].contextual_help.unwrap(), + &BLOCKLIST_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[3].config, None); @@ -155,10 +154,10 @@ mod tests { radarr_data.main_tabs.tabs[4].route, ActiveRadarrBlock::RootFolders.into() ); - assert!(radarr_data.main_tabs.tabs[4].help.is_empty()); + assert!(radarr_data.main_tabs.tabs[4].contextual_help.is_some()); assert_eq!( - radarr_data.main_tabs.tabs[4].contextual_help, - Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)) + radarr_data.main_tabs.tabs[4].contextual_help.unwrap(), + &ROOT_FOLDERS_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[4].config, None); @@ -167,10 +166,10 @@ mod tests { radarr_data.main_tabs.tabs[5].route, ActiveRadarrBlock::Indexers.into() ); - assert!(radarr_data.main_tabs.tabs[5].help.is_empty()); + assert!(radarr_data.main_tabs.tabs[5].contextual_help.is_some()); assert_eq!( - radarr_data.main_tabs.tabs[5].contextual_help, - Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)) + radarr_data.main_tabs.tabs[5].contextual_help.unwrap(), + &INDEXERS_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[5].config, None); @@ -179,10 +178,10 @@ mod tests { radarr_data.main_tabs.tabs[6].route, ActiveRadarrBlock::System.into() ); - assert!(radarr_data.main_tabs.tabs[6].help.is_empty()); + assert!(radarr_data.main_tabs.tabs[6].contextual_help.is_some()); assert_eq!( - radarr_data.main_tabs.tabs[6].contextual_help, - Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)) + radarr_data.main_tabs.tabs[6].contextual_help.unwrap(), + &SYSTEM_CONTEXT_CLUES ); assert_eq!(radarr_data.main_tabs.tabs[6].config, None); @@ -193,13 +192,13 @@ mod tests { radarr_data.movie_info_tabs.tabs[0].route, ActiveRadarrBlock::MovieDetails.into() ); - assert_str_eq!( - radarr_data.movie_info_tabs.tabs[0].help, - build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES) - ); assert!(radarr_data.movie_info_tabs.tabs[0] .contextual_help - .is_none()); + .is_some()); + assert_eq!( + radarr_data.movie_info_tabs.tabs[0].contextual_help.unwrap(), + &MOVIE_DETAILS_CONTEXT_CLUES + ); assert_eq!(radarr_data.movie_info_tabs.tabs[0].config, None); assert_str_eq!(radarr_data.movie_info_tabs.tabs[1].title, "History"); @@ -207,13 +206,13 @@ mod tests { radarr_data.movie_info_tabs.tabs[1].route, ActiveRadarrBlock::MovieHistory.into() ); - assert_str_eq!( - radarr_data.movie_info_tabs.tabs[1].help, - build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES) - ); assert!(radarr_data.movie_info_tabs.tabs[1] .contextual_help - .is_none()); + .is_some()); + assert_eq!( + radarr_data.movie_info_tabs.tabs[1].contextual_help.unwrap(), + &MOVIE_DETAILS_CONTEXT_CLUES + ); assert_eq!(radarr_data.movie_info_tabs.tabs[1].config, None); assert_str_eq!(radarr_data.movie_info_tabs.tabs[2].title, "File"); @@ -221,13 +220,13 @@ mod tests { radarr_data.movie_info_tabs.tabs[2].route, ActiveRadarrBlock::FileInfo.into() ); - assert_str_eq!( - radarr_data.movie_info_tabs.tabs[2].help, - build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES) - ); assert!(radarr_data.movie_info_tabs.tabs[2] .contextual_help - .is_none()); + .is_some()); + assert_eq!( + radarr_data.movie_info_tabs.tabs[2].contextual_help.unwrap(), + &MOVIE_DETAILS_CONTEXT_CLUES + ); assert_eq!(radarr_data.movie_info_tabs.tabs[2].config, None); assert_str_eq!(radarr_data.movie_info_tabs.tabs[3].title, "Cast"); @@ -235,13 +234,13 @@ mod tests { radarr_data.movie_info_tabs.tabs[3].route, ActiveRadarrBlock::Cast.into() ); - assert_str_eq!( - radarr_data.movie_info_tabs.tabs[3].help, - build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES) - ); assert!(radarr_data.movie_info_tabs.tabs[3] .contextual_help - .is_none()); + .is_some()); + assert_eq!( + radarr_data.movie_info_tabs.tabs[3].contextual_help.unwrap(), + &MOVIE_DETAILS_CONTEXT_CLUES + ); assert_eq!(radarr_data.movie_info_tabs.tabs[3].config, None); assert_str_eq!(radarr_data.movie_info_tabs.tabs[4].title, "Crew"); @@ -249,13 +248,13 @@ mod tests { radarr_data.movie_info_tabs.tabs[4].route, ActiveRadarrBlock::Crew.into() ); - assert_str_eq!( - radarr_data.movie_info_tabs.tabs[4].help, - build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES) - ); assert!(radarr_data.movie_info_tabs.tabs[4] .contextual_help - .is_none()); + .is_some()); + assert_eq!( + radarr_data.movie_info_tabs.tabs[4].contextual_help.unwrap(), + &MOVIE_DETAILS_CONTEXT_CLUES + ); assert_eq!(radarr_data.movie_info_tabs.tabs[4].config, None); assert_str_eq!(radarr_data.movie_info_tabs.tabs[5].title, "Manual Search"); @@ -263,15 +262,12 @@ mod tests { radarr_data.movie_info_tabs.tabs[5].route, ActiveRadarrBlock::ManualSearch.into() ); - assert_str_eq!( - radarr_data.movie_info_tabs.tabs[5].help, - build_context_clue_string(&MANUAL_MOVIE_SEARCH_CONTEXT_CLUES) - ); + assert!(radarr_data.movie_info_tabs.tabs[5] + .contextual_help + .is_some()); assert_eq!( - radarr_data.movie_info_tabs.tabs[5].contextual_help, - Some(build_context_clue_string( - &MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES - )) + radarr_data.movie_info_tabs.tabs[5].contextual_help.unwrap(), + &MANUAL_MOVIE_SEARCH_CONTEXT_CLUES ); assert_eq!(radarr_data.movie_info_tabs.tabs[5].config, None); } diff --git a/src/models/servarr_data/servarr_data_tests.rs b/src/models/servarr_data/servarr_data_tests.rs new file mode 100644 index 0000000..5ee250e --- /dev/null +++ b/src/models/servarr_data/servarr_data_tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +mod tests { + use crate::models::servarr_data::ActiveKeybindingBlock; + use crate::models::Route; + use pretty_assertions::assert_eq; + + #[test] + fn test_from_active_keybinding_block_to_route() { + assert_eq!(Route::from(ActiveKeybindingBlock::Help), Route::Keybindings); + } +} diff --git a/src/models/servarr_data/sonarr/modals.rs b/src/models/servarr_data/sonarr/modals.rs index e74b201..924c5ac 100644 --- a/src/models/servarr_data/sonarr/modals.rs +++ b/src/models/servarr_data/sonarr/modals.rs @@ -1,16 +1,12 @@ use strum::IntoEnumIterator; use super::sonarr_data::{ActiveSonarrBlock, SonarrData}; +use crate::app::sonarr::sonarr_context_clues::SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES; use crate::models::sonarr_models::EpisodeFile; use crate::{ - app::{ - context_clues::build_context_clue_string, - sonarr::sonarr_context_clues::{ - DETAILS_CONTEXTUAL_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES, - MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES, - SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, - SEASON_HISTORY_CONTEXT_CLUES, - }, + app::sonarr::sonarr_context_clues::{ + EPISODE_DETAILS_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, + MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, }, models::{ servarr_data::modals::EditIndexerModal, @@ -282,29 +278,25 @@ impl Default for EpisodeDetailsModal { TabRoute { title: "Details".to_string(), route: ActiveSonarrBlock::EpisodeDetails.into(), - help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), - contextual_help: None, + contextual_help: Some(&EPISODE_DETAILS_CONTEXT_CLUES), config: None, }, TabRoute { title: "History".to_string(), route: ActiveSonarrBlock::EpisodeHistory.into(), - help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), - contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), + contextual_help: Some(&SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES), config: None, }, TabRoute { title: "File".to_string(), route: ActiveSonarrBlock::EpisodeFile.into(), - help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), - contextual_help: None, + contextual_help: Some(&EPISODE_DETAILS_CONTEXT_CLUES), config: None, }, TabRoute { title: "Manual Search".to_string(), route: ActiveSonarrBlock::ManualEpisodeSearch.into(), - help: build_context_clue_string(&MANUAL_EPISODE_SEARCH_CONTEXT_CLUES), - contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), + contextual_help: Some(&MANUAL_EPISODE_SEARCH_CONTEXT_CLUES), config: None, }, ]), @@ -333,24 +325,19 @@ impl Default for SeasonDetailsModal { TabRoute { title: "Episodes".to_string(), route: ActiveSonarrBlock::SeasonDetails.into(), - help: build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES), - contextual_help: Some(build_context_clue_string( - &SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, - )), + contextual_help: Some(&SEASON_DETAILS_CONTEXT_CLUES), config: None, }, TabRoute { title: "History".to_string(), route: ActiveSonarrBlock::SeasonHistory.into(), - help: build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES), - contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), + contextual_help: Some(&SEASON_HISTORY_CONTEXT_CLUES), config: None, }, TabRoute { title: "Manual Search".to_string(), route: ActiveSonarrBlock::ManualSeasonSearch.into(), - help: build_context_clue_string(&MANUAL_SEASON_SEARCH_CONTEXT_CLUES), - contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)), + contextual_help: Some(&MANUAL_SEASON_SEARCH_CONTEXT_CLUES), config: None, }, ]), diff --git a/src/models/servarr_data/sonarr/modals_tests.rs b/src/models/servarr_data/sonarr/modals_tests.rs index 5ab6fc2..5aa259a 100644 --- a/src/models/servarr_data/sonarr/modals_tests.rs +++ b/src/models/servarr_data/sonarr/modals_tests.rs @@ -5,12 +5,10 @@ mod tests { use rstest::rstest; use strum::IntoEnumIterator; - use crate::app::context_clues::build_context_clue_string; use crate::app::sonarr::sonarr_context_clues::{ - DETAILS_CONTEXTUAL_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES, - MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES, - SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, - SEASON_HISTORY_CONTEXT_CLUES, + EPISODE_DETAILS_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, + MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, + SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES, }; use crate::models::servarr_data::sonarr::modals::{ EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal, @@ -257,13 +255,15 @@ mod tests { episode_details_modal.episode_details_tabs.tabs[0].route, ActiveSonarrBlock::EpisodeDetails.into() ); - assert_str_eq!( - episode_details_modal.episode_details_tabs.tabs[0].help, - build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES) - ); assert!(episode_details_modal.episode_details_tabs.tabs[0] .contextual_help - .is_none()); + .is_some()); + assert_eq!( + episode_details_modal.episode_details_tabs.tabs[0] + .contextual_help + .unwrap(), + &EPISODE_DETAILS_CONTEXT_CLUES + ); assert_eq!( episode_details_modal.episode_details_tabs.tabs[0].config, None @@ -277,13 +277,14 @@ mod tests { episode_details_modal.episode_details_tabs.tabs[1].route, ActiveSonarrBlock::EpisodeHistory.into() ); - assert_str_eq!( - episode_details_modal.episode_details_tabs.tabs[1].help, - build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES) - ); + assert!(episode_details_modal.episode_details_tabs.tabs[1] + .contextual_help + .is_some()); assert_eq!( - episode_details_modal.episode_details_tabs.tabs[1].contextual_help, - Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)) + episode_details_modal.episode_details_tabs.tabs[1] + .contextual_help + .unwrap(), + &SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES ); assert_eq!( episode_details_modal.episode_details_tabs.tabs[1].config, @@ -298,13 +299,15 @@ mod tests { episode_details_modal.episode_details_tabs.tabs[2].route, ActiveSonarrBlock::EpisodeFile.into() ); - assert_str_eq!( - episode_details_modal.episode_details_tabs.tabs[2].help, - build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES) - ); assert!(episode_details_modal.episode_details_tabs.tabs[2] .contextual_help - .is_none()); + .is_some()); + assert_eq!( + episode_details_modal.episode_details_tabs.tabs[2] + .contextual_help + .unwrap(), + &EPISODE_DETAILS_CONTEXT_CLUES + ); assert_eq!( episode_details_modal.episode_details_tabs.tabs[2].config, None @@ -318,13 +321,14 @@ mod tests { episode_details_modal.episode_details_tabs.tabs[3].route, ActiveSonarrBlock::ManualEpisodeSearch.into() ); - assert_str_eq!( - episode_details_modal.episode_details_tabs.tabs[3].help, - build_context_clue_string(&MANUAL_EPISODE_SEARCH_CONTEXT_CLUES) - ); + assert!(episode_details_modal.episode_details_tabs.tabs[3] + .contextual_help + .is_some()); assert_eq!( - episode_details_modal.episode_details_tabs.tabs[3].contextual_help, - Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)) + episode_details_modal.episode_details_tabs.tabs[3] + .contextual_help + .unwrap(), + &MANUAL_EPISODE_SEARCH_CONTEXT_CLUES ); assert_eq!( episode_details_modal.episode_details_tabs.tabs[3].config, @@ -352,15 +356,14 @@ mod tests { season_details_modal.season_details_tabs.tabs[0].route, ActiveSonarrBlock::SeasonDetails.into() ); - assert_str_eq!( - season_details_modal.season_details_tabs.tabs[0].help, - build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES) - ); + assert!(season_details_modal.season_details_tabs.tabs[0] + .contextual_help + .is_some()); assert_eq!( - season_details_modal.season_details_tabs.tabs[0].contextual_help, - Some(build_context_clue_string( - &SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES - )) + season_details_modal.season_details_tabs.tabs[0] + .contextual_help + .unwrap(), + &SEASON_DETAILS_CONTEXT_CLUES ); assert_eq!( season_details_modal.season_details_tabs.tabs[0].config, @@ -375,13 +378,14 @@ mod tests { season_details_modal.season_details_tabs.tabs[1].route, ActiveSonarrBlock::SeasonHistory.into() ); - assert_str_eq!( - season_details_modal.season_details_tabs.tabs[1].help, - build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES) - ); + assert!(season_details_modal.season_details_tabs.tabs[1] + .contextual_help + .is_some()); assert_eq!( - season_details_modal.season_details_tabs.tabs[1].contextual_help, - Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)) + season_details_modal.season_details_tabs.tabs[1] + .contextual_help + .unwrap(), + &SEASON_HISTORY_CONTEXT_CLUES ); assert_eq!( season_details_modal.season_details_tabs.tabs[1].config, @@ -396,13 +400,14 @@ mod tests { season_details_modal.season_details_tabs.tabs[2].route, ActiveSonarrBlock::ManualSeasonSearch.into() ); - assert_str_eq!( - season_details_modal.season_details_tabs.tabs[2].help, - build_context_clue_string(&MANUAL_SEASON_SEARCH_CONTEXT_CLUES) - ); + assert!(season_details_modal.season_details_tabs.tabs[2] + .contextual_help + .is_some()); assert_eq!( - season_details_modal.season_details_tabs.tabs[2].contextual_help, - Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)) + season_details_modal.season_details_tabs.tabs[2] + .contextual_help + .unwrap(), + &MANUAL_SEASON_SEARCH_CONTEXT_CLUES ); assert_eq!( season_details_modal.season_details_tabs.tabs[2].config, diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index 3275e91..6caa97d 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -5,8 +5,8 @@ use strum::EnumIter; use crate::{ app::{ context_clues::{ - build_context_clue_string, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, - INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }, sonarr::sonarr_context_clues::{ HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, @@ -130,50 +130,43 @@ impl<'a> Default for SonarrData<'a> { TabRoute { title: "Library".to_string(), route: ActiveSonarrBlock::Series.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&SERIES_CONTEXT_CLUES)), + contextual_help: Some(&SERIES_CONTEXT_CLUES), config: None, }, TabRoute { title: "Downloads".to_string(), route: ActiveSonarrBlock::Downloads.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)), + contextual_help: Some(&DOWNLOADS_CONTEXT_CLUES), config: None, }, TabRoute { title: "Blocklist".to_string(), route: ActiveSonarrBlock::Blocklist.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)), + contextual_help: Some(&BLOCKLIST_CONTEXT_CLUES), config: None, }, TabRoute { title: "History".to_string(), route: ActiveSonarrBlock::History.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)), + contextual_help: Some(&HISTORY_CONTEXT_CLUES), config: None, }, TabRoute { title: "Root Folders".to_string(), route: ActiveSonarrBlock::RootFolders.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)), + contextual_help: Some(&ROOT_FOLDERS_CONTEXT_CLUES), config: None, }, TabRoute { title: "Indexers".to_string(), route: ActiveSonarrBlock::Indexers.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)), + contextual_help: Some(&INDEXERS_CONTEXT_CLUES), config: None, }, TabRoute { title: "System".to_string(), route: ActiveSonarrBlock::System.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)), + contextual_help: Some(&SYSTEM_CONTEXT_CLUES), config: None, }, ]), @@ -181,15 +174,13 @@ impl<'a> Default for SonarrData<'a> { TabRoute { title: "Seasons".to_string(), route: ActiveSonarrBlock::SeriesDetails.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&SERIES_DETAILS_CONTEXT_CLUES)), + contextual_help: Some(&SERIES_DETAILS_CONTEXT_CLUES), config: None, }, TabRoute { title: "History".to_string(), route: ActiveSonarrBlock::SeriesHistory.into(), - help: String::new(), - contextual_help: Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES)), + contextual_help: Some(&SERIES_HISTORY_CONTEXT_CLUES), config: None, }, ]), diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs index 2ddd1e8..9b666dd 100644 --- a/src/models/servarr_data/sonarr/sonarr_data_tests.rs +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -10,8 +10,8 @@ mod tests { use crate::{ app::{ context_clues::{ - build_context_clue_string, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, - INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, + BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, + ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }, sonarr::sonarr_context_clues::{ HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, @@ -123,10 +123,10 @@ mod tests { sonarr_data.main_tabs.tabs[0].route, ActiveSonarrBlock::Series.into() ); - assert!(sonarr_data.main_tabs.tabs[0].help.is_empty()); + assert!(sonarr_data.main_tabs.tabs[0].contextual_help.is_some()); assert_eq!( - sonarr_data.main_tabs.tabs[0].contextual_help, - Some(build_context_clue_string(&SERIES_CONTEXT_CLUES)) + sonarr_data.main_tabs.tabs[0].contextual_help.unwrap(), + &SERIES_CONTEXT_CLUES ); assert_eq!(sonarr_data.main_tabs.tabs[0].config, None); @@ -135,10 +135,10 @@ mod tests { sonarr_data.main_tabs.tabs[1].route, ActiveSonarrBlock::Downloads.into() ); - assert!(sonarr_data.main_tabs.tabs[1].help.is_empty()); + assert!(sonarr_data.main_tabs.tabs[1].contextual_help.is_some()); assert_eq!( - sonarr_data.main_tabs.tabs[1].contextual_help, - Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)) + sonarr_data.main_tabs.tabs[1].contextual_help.unwrap(), + &DOWNLOADS_CONTEXT_CLUES ); assert_eq!(sonarr_data.main_tabs.tabs[1].config, None); @@ -147,10 +147,10 @@ mod tests { sonarr_data.main_tabs.tabs[2].route, ActiveSonarrBlock::Blocklist.into() ); - assert!(sonarr_data.main_tabs.tabs[2].help.is_empty()); + assert!(sonarr_data.main_tabs.tabs[2].contextual_help.is_some()); assert_eq!( - sonarr_data.main_tabs.tabs[2].contextual_help, - Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)) + sonarr_data.main_tabs.tabs[2].contextual_help.unwrap(), + &BLOCKLIST_CONTEXT_CLUES ); assert_eq!(sonarr_data.main_tabs.tabs[2].config, None); @@ -159,10 +159,10 @@ mod tests { sonarr_data.main_tabs.tabs[3].route, ActiveSonarrBlock::History.into() ); - assert!(sonarr_data.main_tabs.tabs[3].help.is_empty()); + assert!(sonarr_data.main_tabs.tabs[3].contextual_help.is_some()); assert_eq!( - sonarr_data.main_tabs.tabs[3].contextual_help, - Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)) + sonarr_data.main_tabs.tabs[3].contextual_help.unwrap(), + &HISTORY_CONTEXT_CLUES ); assert_eq!(sonarr_data.main_tabs.tabs[3].config, None); @@ -171,10 +171,10 @@ mod tests { sonarr_data.main_tabs.tabs[4].route, ActiveSonarrBlock::RootFolders.into() ); - assert!(sonarr_data.main_tabs.tabs[4].help.is_empty()); + assert!(sonarr_data.main_tabs.tabs[4].contextual_help.is_some()); assert_eq!( - sonarr_data.main_tabs.tabs[4].contextual_help, - Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)) + sonarr_data.main_tabs.tabs[4].contextual_help.unwrap(), + &ROOT_FOLDERS_CONTEXT_CLUES ); assert_eq!(sonarr_data.main_tabs.tabs[4].config, None); @@ -183,10 +183,10 @@ mod tests { sonarr_data.main_tabs.tabs[5].route, ActiveSonarrBlock::Indexers.into() ); - assert!(sonarr_data.main_tabs.tabs[5].help.is_empty()); + assert!(sonarr_data.main_tabs.tabs[5].contextual_help.is_some()); assert_eq!( - sonarr_data.main_tabs.tabs[5].contextual_help, - Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)) + sonarr_data.main_tabs.tabs[5].contextual_help.unwrap(), + &INDEXERS_CONTEXT_CLUES ); assert_eq!(sonarr_data.main_tabs.tabs[5].config, None); @@ -195,10 +195,10 @@ mod tests { sonarr_data.main_tabs.tabs[6].route, ActiveSonarrBlock::System.into() ); - assert!(sonarr_data.main_tabs.tabs[6].help.is_empty()); + assert!(sonarr_data.main_tabs.tabs[6].contextual_help.is_some()); assert_eq!( - sonarr_data.main_tabs.tabs[6].contextual_help, - Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)) + sonarr_data.main_tabs.tabs[6].contextual_help.unwrap(), + &SYSTEM_CONTEXT_CLUES ); assert_eq!(sonarr_data.main_tabs.tabs[6].config, None); @@ -209,10 +209,14 @@ mod tests { sonarr_data.series_info_tabs.tabs[0].route, ActiveSonarrBlock::SeriesDetails.into() ); - assert!(sonarr_data.series_info_tabs.tabs[0].help.is_empty()); + assert!(sonarr_data.series_info_tabs.tabs[0] + .contextual_help + .is_some()); assert_eq!( - sonarr_data.series_info_tabs.tabs[0].contextual_help, - Some(build_context_clue_string(&SERIES_DETAILS_CONTEXT_CLUES)) + sonarr_data.series_info_tabs.tabs[0] + .contextual_help + .unwrap(), + &SERIES_DETAILS_CONTEXT_CLUES ); assert_eq!(sonarr_data.series_info_tabs.tabs[0].config, None); @@ -221,10 +225,14 @@ mod tests { sonarr_data.series_info_tabs.tabs[1].route, ActiveSonarrBlock::SeriesHistory.into() ); - assert!(sonarr_data.series_info_tabs.tabs[1].help.is_empty()); + assert!(sonarr_data.series_info_tabs.tabs[1] + .contextual_help + .is_some()); assert_eq!( - sonarr_data.series_info_tabs.tabs[1].contextual_help, - Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES)) + sonarr_data.series_info_tabs.tabs[1] + .contextual_help + .unwrap(), + &SERIES_HISTORY_CONTEXT_CLUES ); assert_eq!(sonarr_data.series_info_tabs.tabs[1].config, None); } diff --git a/src/models/servarr_models.rs b/src/models/servarr_models.rs index 3a8b740..1be94d8 100644 --- a/src/models/servarr_models.rs +++ b/src/models/servarr_models.rs @@ -107,6 +107,13 @@ pub struct EditIndexerParams { pub clear_tags: bool, } +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct KeybindingItem { + pub key: String, + pub alt_key: String, + pub desc: String, +} + #[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct HostConfig { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9d3dd64..78a89f2 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -4,15 +4,16 @@ use std::sync::atomic::Ordering; use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::{Line, Text}; -use ratatui::widgets::Clear; use ratatui::widgets::Paragraph; use ratatui::widgets::Tabs; use ratatui::widgets::Wrap; +use ratatui::widgets::{Clear, Row}; use ratatui::Frame; use sonarr_ui::SonarrUi; use utils::layout_block; use crate::app::App; +use crate::models::servarr_models::KeybindingItem; use crate::models::{HorizontallyScrollableText, Route, TabState}; use crate::ui::radarr_ui::RadarrUi; use crate::ui::styles::ManagarrStyle; @@ -22,6 +23,7 @@ use crate::ui::utils::{ unstyled_title_block, }; use crate::ui::widgets::input_box::InputBox; +use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::popup::Size; mod builtin_themes; @@ -79,6 +81,10 @@ pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) { } _ => (), } + + if app.keymapping_table.is_some() { + draw_help_popup(f, app); + } } fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { @@ -88,7 +94,7 @@ fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .flex(Flex::SpaceBetween) .margin(1) .areas(area); - let help_text = Text::from(app.server_tabs.get_active_tab_help().help()); + let help_text = Text::from(" to open help".help()); let titles = app .server_tabs @@ -138,6 +144,34 @@ pub fn draw_popup( popup_fn(f, app, popup_area); } +pub fn draw_help_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let (percent_x, percent_y) = Size::LongNarrowTable.to_percent(); + let table_area = centered_rect(percent_x, percent_y, f.area()); + let keymap_row_mapping = |keymap: &KeybindingItem| { + Row::new(vec![ + ratatui::widgets::Cell::from(keymap.key.clone()), + ratatui::widgets::Cell::from(keymap.alt_key.clone()), + ratatui::widgets::Cell::from(keymap.desc.clone()), + ]) + .primary() + }; + let keymapping_table = ManagarrTable::new( + Some(app.keymapping_table.as_mut().unwrap()), + keymap_row_mapping, + ) + .block(title_block("Keybindings")) + .loading(app.is_loading) + .headers(["Key", "Alt Key", "Description"]) + .constraints([ + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + ]); + f.render_widget(Clear, table_area); + f.render_widget(background_block(), table_area); + f.render_widget(keymapping_table, table_area); +} + fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -> Rect { if title.is_empty() { f.render_widget(layout_block().default(), area); @@ -148,8 +182,6 @@ fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) - let [header_area, content_area] = Layout::vertical([Constraint::Length(1), Constraint::Fill(0)]) .margin(1) .areas(area); - let [tabs_area, help_area] = - Layout::horizontal([Constraint::Percentage(45), Constraint::Fill(0)]).areas(header_area); let titles = tab_state .tabs @@ -159,12 +191,8 @@ fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) - .block(borderless_block()) .highlight_style(Style::new().secondary()) .select(tab_state.index); - let help = Paragraph::new(Text::from(tab_state.get_active_tab_help().help())) - .block(borderless_block()) - .right_aligned(); - f.render_widget(tabs, tabs_area); - f.render_widget(help, help_area); + f.render_widget(tabs, header_area); content_area } diff --git a/src/ui/radarr_ui/blocklist/mod.rs b/src/ui/radarr_ui/blocklist/mod.rs index b6b2b57..c6b30f0 100644 --- a/src/ui/radarr_ui/blocklist/mod.rs +++ b/src/ui/radarr_ui/blocklist/mod.rs @@ -82,11 +82,6 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { app.data.radarr_data.blocklist.current_selection().clone() }; - let blocklist_table_footer = app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(); let blocklist_row_mapping = |blocklist_item: &BlocklistItem| { let BlocklistItem { @@ -136,7 +131,6 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(blocklist_table_footer) .sorting(active_radarr_block == ActiveRadarrBlock::BlocklistSortPrompt) .headers([ "Movie Title", diff --git a/src/ui/radarr_ui/collections/collection_details_ui.rs b/src/ui/radarr_ui/collections/collection_details_ui.rs index 2b16be6..34118f4 100644 --- a/src/ui/radarr_ui/collections/collection_details_ui.rs +++ b/src/ui/radarr_ui/collections/collection_details_ui.rs @@ -4,8 +4,6 @@ use ratatui::text::{Line, Text}; use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; use ratatui::Frame; -use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES}; -use crate::app::radarr::radarr_context_clues::COLLECTION_DETAILS_CONTEXT_CLUES; use crate::app::App; use crate::models::radarr_models::CollectionMovie; use crate::models::servarr_data::radarr::radarr_data::{ @@ -144,10 +142,6 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) "No" }; let minimum_availability = collection_selection.minimum_availability.to_display_str(); - let help_footer = format!( - "<↑↓> scroll table | {}", - build_context_clue_string(&COLLECTION_DETAILS_CONTEXT_CLUES) - ); let collection_description = Text::from(vec![ Line::from(vec![ @@ -191,7 +185,6 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) .block(layout_block_top_border_with_title(title_style("Movies"))) .loading(app.is_loading) .footer_alignment(Alignment::Center) - .footer(Some(help_footer)) .headers([ "✔", "Title", @@ -220,11 +213,10 @@ fn draw_movie_overview(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let title_block = title_block("Overview"); f.render_widget(title_block, area); - let [paragraph_area, help_area] = - Layout::vertical([Constraint::Percentage(95), Constraint::Length(1)]) - .flex(Flex::SpaceBetween) - .margin(1) - .areas(area); + let [paragraph_area] = Layout::vertical([Constraint::Percentage(95)]) + .flex(Flex::SpaceBetween) + .margin(1) + .areas(area); let overview = Text::from( app .data @@ -235,15 +227,10 @@ fn draw_movie_overview(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .overview, ) .default(); - let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help()); let paragraph = Paragraph::new(overview) .block(borderless_block()) .wrap(Wrap { trim: false }); - let help_paragraph = Paragraph::new(help_text) - .block(borderless_block()) - .centered(); f.render_widget(paragraph, paragraph_area); - f.render_widget(help_paragraph, help_area); } diff --git a/src/ui/radarr_ui/collections/edit_collection_ui.rs b/src/ui/radarr_ui/collections/edit_collection_ui.rs index 5e781cf..407eb23 100644 --- a/src/ui/radarr_ui/collections/edit_collection_ui.rs +++ b/src/ui/radarr_ui/collections/edit_collection_ui.rs @@ -1,10 +1,8 @@ use ratatui::layout::{Constraint, Layout, Rect}; -use ratatui::text::Text; -use ratatui::widgets::{ListItem, Paragraph}; +use ratatui::widgets::ListItem; use ratatui::Frame; use std::sync::atomic::Ordering; -use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::radarr::modals::EditCollectionModal; use crate::models::servarr_data::radarr::radarr_data::{ @@ -102,7 +100,7 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_> let selected_minimum_availability = minimum_availability_list.current_selection(); let selected_quality_profile = quality_profile_list.current_selection(); - let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, root_folder_area, search_on_add_area, _, buttons_area, help_area] = + let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, root_folder_area, search_on_add_area, _, buttons_area] = Layout::vertical([ Constraint::Length(6), Constraint::Length(3), @@ -112,7 +110,6 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_> Constraint::Length(3), Constraint::Fill(1), Constraint::Length(3), - Constraint::Length(1), ]) .margin(1) .areas(area); @@ -120,8 +117,6 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_> Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) .areas(buttons_area); - let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text).centered(); let prompt_paragraph = layout_paragraph_borderless(&collection_overview); let monitored_checkbox = Checkbox::new("Monitored") .highlighted(selected_block == ActiveRadarrBlock::EditCollectionToggleMonitored) @@ -163,7 +158,6 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_> f.render_widget(search_on_add_checkbox, search_on_add_area); f.render_widget(save_button, save_area); f.render_widget(cancel_button, cancel_area); - f.render_widget(help_paragraph, help_area); } fn draw_edit_collection_select_minimum_availability_popup(f: &mut Frame<'_>, app: &mut App<'_>) { diff --git a/src/ui/radarr_ui/collections/mod.rs b/src/ui/radarr_ui/collections/mod.rs index 37269f6..90034e7 100644 --- a/src/ui/radarr_ui/collections/mod.rs +++ b/src/ui/radarr_ui/collections/mod.rs @@ -66,11 +66,6 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) }; let quality_profile_map = &app.data.radarr_data.quality_profile_map; let content = Some(&mut app.data.radarr_data.collections); - let collections_table_footer = app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(); let collection_row_mapping = |collection: &Collection| { let number_of_movies = collection.movies.as_ref().unwrap_or(&Vec::new()).len(); collection.title.scroll_left_or_reset( @@ -112,7 +107,6 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) || app.data.radarr_data.movies.is_empty() || app.data.radarr_data.quality_profile_map.is_empty(), ) - .footer(collections_table_footer) .block(layout_block_top_border()) .sorting(active_radarr_block == ActiveRadarrBlock::CollectionsSortPrompt) .searching(active_radarr_block == ActiveRadarrBlock::SearchCollection) diff --git a/src/ui/radarr_ui/downloads/mod.rs b/src/ui/radarr_ui/downloads/mod.rs index d247ff8..6f27f39 100644 --- a/src/ui/radarr_ui/downloads/mod.rs +++ b/src/ui/radarr_ui/downloads/mod.rs @@ -72,11 +72,6 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { app.data.radarr_data.downloads.current_selection().clone() }; - let downloads_table_footer = app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(); let downloads_row_mapping = |download_record: &DownloadRecord| { let DownloadRecord { @@ -125,7 +120,6 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(downloads_table_footer) .headers([ "Title", "Percent Complete", diff --git a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs index a4a2282..85fc58a 100644 --- a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs +++ b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs @@ -1,6 +1,5 @@ use std::sync::atomic::Ordering; -use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS}; use crate::models::Route; @@ -14,8 +13,6 @@ use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::popup::Size; use crate::ui::{draw_popup, DrawUi}; use ratatui::layout::{Constraint, Flex, Layout, Rect}; -use ratatui::text::Text; -use ratatui::widgets::Paragraph; use ratatui::Frame; #[cfg(test)] @@ -45,22 +42,15 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let highlight_yes_no = selected_block == ActiveRadarrBlock::EditIndexerConfirmPrompt; let edit_indexer_modal_option = &app.data.radarr_data.edit_indexer_modal; let protocol = &app.data.radarr_data.indexers.current_selection().protocol; - let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text).centered(); if edit_indexer_modal_option.is_some() { f.render_widget(block, area); let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap(); - let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([ - Constraint::Fill(1), - Constraint::Length(18), - Constraint::Fill(1), - Constraint::Length(3), - Constraint::Length(1), - ]) - .margin(1) - .areas(area); + let [settings_area, buttons_area] = + Layout::vertical([Constraint::Fill(1), Constraint::Length(3)]) + .margin(1) + .areas(area); let [left_side_area, right_side_area] = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) .margin(1) @@ -169,7 +159,6 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f.render_widget(interactive_search_checkbox, interactive_search_area); f.render_widget(save_button, save_area); f.render_widget(cancel_button, cancel_area); - f.render_widget(help_paragraph, help_area); } } else { f.render_widget(LoadingBlock::new(app.is_loading, block), area); diff --git a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs index 0c2fe2e..1aa709c 100644 --- a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs +++ b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs @@ -1,11 +1,8 @@ use std::sync::atomic::Ordering; use ratatui::layout::{Constraint, Flex, Layout, Rect}; -use ratatui::text::Text; -use ratatui::widgets::Paragraph; use ratatui::Frame; -use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS, @@ -52,22 +49,15 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: let selected_block = app.data.radarr_data.selected_block.get_active_block(); let highlight_yes_no = selected_block == ActiveRadarrBlock::IndexerSettingsConfirmPrompt; let indexer_settings_option = &app.data.radarr_data.indexer_settings; - let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text).centered(); if indexer_settings_option.is_some() { f.render_widget(block, area); let indexer_settings = indexer_settings_option.as_ref().unwrap(); - let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([ - Constraint::Fill(1), - Constraint::Length(15), - Constraint::Fill(1), - Constraint::Length(3), - Constraint::Length(1), - ]) - .margin(1) - .areas(area); + let [settings_area, buttons_area] = + Layout::vertical([Constraint::Fill(1), Constraint::Length(3)]) + .margin(1) + .areas(area); let [left_side_area, right_side_area] = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) .margin(1) @@ -167,7 +157,6 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: f.render_widget(allow_hardcoded_subs_checkbox, allow_hardcoded_subs_area); f.render_widget(save_button, save_area); f.render_widget(cancel_button, cancel_area); - f.render_widget(help_paragraph, help_area); } else { f.render_widget(LoadingBlock::new(app.is_loading, block), area); } diff --git a/src/ui/radarr_ui/indexers/mod.rs b/src/ui/radarr_ui/indexers/mod.rs index a6fcb9b..d1864a6 100644 --- a/src/ui/radarr_ui/indexers/mod.rs +++ b/src/ui/radarr_ui/indexers/mod.rs @@ -157,17 +157,11 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let indexers_table_footer = app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(); let indexers_table = ManagarrTable::new( Some(&mut app.data.radarr_data.indexers), indexers_row_mapping, ) .block(layout_block_top_border()) - .footer(indexers_table_footer) .loading(app.is_loading) .headers([ "Indexer", diff --git a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs index 9dcb983..e2b70aa 100644 --- a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs +++ b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs @@ -1,4 +1,3 @@ -use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; @@ -43,10 +42,6 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are IndexerTestResultModalItem::default() }; f.render_widget(block, area); - let help_footer = format!( - "<↑↓> scroll | {}", - build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) - ); let test_results_row_mapping = |result: &IndexerTestResultModalItem| { result.validation_failures.scroll_left_or_reset( get_width_from_percentage(area, 86), @@ -72,7 +67,6 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are test_results_row_mapping, ) .loading(is_loading) - .footer(Some(help_footer)) .footer_alignment(Alignment::Center) .margin(1) .headers(["Indexer", "Pass/Fail", "Failure Messages"]) diff --git a/src/ui/radarr_ui/library/add_movie_ui.rs b/src/ui/radarr_ui/library/add_movie_ui.rs index 9032eb3..f981e52 100644 --- a/src/ui/radarr_ui/library/add_movie_ui.rs +++ b/src/ui/radarr_ui/library/add_movie_ui.rs @@ -1,14 +1,9 @@ use std::sync::atomic::Ordering; use ratatui::layout::{Constraint, Layout, Rect}; -use ratatui::text::Text; -use ratatui::widgets::{Cell, ListItem, Paragraph, Row}; +use ratatui::widgets::{Cell, ListItem, Row}; use ratatui::Frame; -use crate::app::context_clues::{ - build_context_clue_string, BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, -}; -use crate::app::radarr::radarr_context_clues::ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES; use crate::models::radarr_models::AddMovieSearchResult; use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS}; @@ -16,8 +11,7 @@ use crate::models::Route; use crate::ui::radarr_ui::collections::CollectionsUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ - borderless_block, get_width_from_percentage, layout_block, layout_paragraph_borderless, - title_block_centered, + get_width_from_percentage, layout_block, layout_paragraph_borderless, title_block_centered, }; use crate::ui::widgets::button::Button; use crate::ui::widgets::input_box::InputBox; @@ -83,13 +77,10 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { AddMovieSearchResult::default() }; - let [search_box_area, results_area, help_area] = Layout::vertical([ - Constraint::Length(3), - Constraint::Fill(0), - Constraint::Length(3), - ]) - .margin(1) - .areas(area); + let [search_box_area, results_area] = + Layout::vertical([Constraint::Length(3), Constraint::Fill(0)]) + .margin(1) + .areas(area); let block_content = &app.data.radarr_data.add_movie_search.as_ref().unwrap().text; let offset = app .data @@ -164,27 +155,17 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let search_box = InputBox::new(block_content) .offset(offset) .block(title_block_centered("Add Movie")); - let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text) - .block(borderless_block()) - .centered(); search_box.show_cursor(f, search_box_area); f.render_widget(layout_block().default(), results_area); f.render_widget(search_box, search_box_area); - f.render_widget(help_paragraph, help_area); } ActiveRadarrBlock::AddMovieEmptySearchResults => { - let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text) - .block(borderless_block()) - .centered(); let error_message = Message::new("No movies found matching your query!"); let error_message_popup = Popup::new(error_message).size(Size::Message); f.render_widget(layout_block().default(), results_area); f.render_widget(error_message_popup, f.area()); - f.render_widget(help_paragraph, help_area); } ActiveRadarrBlock::AddMovieSearchResults | ActiveRadarrBlock::AddMoviePrompt @@ -194,11 +175,6 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { | ActiveRadarrBlock::AddMovieSelectRootFolder | ActiveRadarrBlock::AddMovieAlreadyInLibrary | ActiveRadarrBlock::AddMovieTagsInput => { - let help_text = - Text::from(build_context_clue_string(&ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text) - .block(borderless_block()) - .centered(); let search_results_table = ManagarrTable::new( app.data.radarr_data.add_searched_movies.as_mut(), search_results_row_mapping, @@ -225,7 +201,6 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]); f.render_widget(search_results_table, results_area); - f.render_widget(help_paragraph, help_area); } _ => (), } @@ -322,7 +297,7 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f.render_widget(title_block_centered(&title), area); - let [paragraph_area, root_folder_area, monitor_area, min_availability_area, quality_profile_area, tags_area, _, buttons_area, help_area] = + let [paragraph_area, root_folder_area, monitor_area, min_availability_area, quality_profile_area, tags_area, _, buttons_area] = Layout::vertical([ Constraint::Length(6), Constraint::Length(3), @@ -332,16 +307,12 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Constraint::Length(3), Constraint::Fill(1), Constraint::Length(3), - Constraint::Length(1), ]) .margin(1) .areas(area); let prompt_paragraph = layout_paragraph_borderless(&prompt); - let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text).centered(); f.render_widget(prompt_paragraph, paragraph_area); - f.render_widget(help_paragraph, help_area); let [add_area, cancel_area] = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) diff --git a/src/ui/radarr_ui/library/edit_movie_ui.rs b/src/ui/radarr_ui/library/edit_movie_ui.rs index cb4974c..bc2a8c6 100644 --- a/src/ui/radarr_ui/library/edit_movie_ui.rs +++ b/src/ui/radarr_ui/library/edit_movie_ui.rs @@ -2,11 +2,9 @@ use std::sync::atomic::Ordering; use ratatui::layout::{Constraint, Rect}; use ratatui::prelude::Layout; -use ratatui::text::Text; -use ratatui::widgets::{ListItem, Paragraph}; +use ratatui::widgets::ListItem; use ratatui::Frame; -use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::radarr::modals::EditMovieModal; use crate::models::servarr_data::radarr::radarr_data::{ @@ -94,7 +92,7 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are let selected_minimum_availability = minimum_availability_list.current_selection(); let selected_quality_profile = quality_profile_list.current_selection(); - let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, path_area, tags_area, _, buttons_area, help_area] = + let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, path_area, tags_area, _, buttons_area] = Layout::vertical([ Constraint::Length(6), Constraint::Length(3), @@ -104,7 +102,6 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are Constraint::Length(3), Constraint::Fill(1), Constraint::Length(3), - Constraint::Length(1), ]) .margin(1) .areas(area); @@ -112,8 +109,6 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) .areas(buttons_area); - let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text).centered(); let prompt_paragraph = layout_paragraph_borderless(&movie_overview); let monitored_checkbox = Checkbox::new("Monitored") .checked(monitored.unwrap_or_default()) @@ -164,7 +159,6 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are f.render_widget(quality_profile_drop_down_button, quality_profile_area); f.render_widget(save_button, save_area); f.render_widget(cancel_button, cancel_area); - f.render_widget(help_paragraph, help_area); } fn draw_edit_movie_select_minimum_availability_popup(f: &mut Frame<'_>, app: &mut App<'_>) { diff --git a/src/ui/radarr_ui/library/mod.rs b/src/ui/radarr_ui/library/mod.rs index c74ed8f..c683fd0 100644 --- a/src/ui/radarr_ui/library/mod.rs +++ b/src/ui/radarr_ui/library/mod.rs @@ -85,11 +85,6 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let tags_map = &app.data.radarr_data.tags_map; let downloads_vec = &app.data.radarr_data.downloads.items; let content = Some(&mut app.data.radarr_data.movies); - let help_footer = app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(); let library_table_row_mapping = |movie: &Movie| { movie.title.scroll_left_or_reset( @@ -143,7 +138,6 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let library_table = ManagarrTable::new(content, library_table_row_mapping) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(help_footer) .sorting(active_radarr_block == ActiveRadarrBlock::MoviesSortPrompt) .searching(active_radarr_block == ActiveRadarrBlock::SearchMovie) .search_produced_empty_results(active_radarr_block == ActiveRadarrBlock::SearchMovieError) diff --git a/src/ui/radarr_ui/library/movie_details_ui.rs b/src/ui/radarr_ui/library/movie_details_ui.rs index bbfa732..d7291c0 100644 --- a/src/ui/radarr_ui/library/movie_details_ui.rs +++ b/src/ui/radarr_ui/library/movie_details_ui.rs @@ -259,18 +259,12 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let help_footer = app - .data - .radarr_data - .movie_info_tabs - .get_active_tab_contextual_help(); let history_table = ManagarrTable::new( Some(&mut movie_details_modal.movie_history), history_row_mapping, ) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(help_footer) .headers(["Source Title", "Event Type", "Languages", "Quality", "Date"]) .constraints([ Constraint::Percentage(34), @@ -301,14 +295,8 @@ fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .primary() }; let content = Some(&mut movie_details_modal.movie_cast); - let help_footer = app - .data - .radarr_data - .movie_info_tabs - .get_active_tab_contextual_help(); let cast_table = ManagarrTable::new(content, cast_row_mapping) .block(layout_block_top_border()) - .footer(help_footer) .loading(app.is_loading) .headers(["Cast Member", "Character"]) .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); @@ -344,17 +332,11 @@ fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .primary() }; let content = Some(&mut movie_details_modal.movie_crew); - let help_footer = app - .data - .radarr_data - .movie_info_tabs - .get_active_tab_contextual_help(); let crew_table = ManagarrTable::new(content, crew_row_mapping) .block(layout_block_top_border()) .loading(app.is_loading) .headers(["Crew Member", "Job", "Department"]) - .constraints(iter::repeat_n(Constraint::Ratio(1, 3), 3)) - .footer(help_footer); + .constraints(iter::repeat_n(Constraint::Ratio(1, 3), 3)); f.render_widget(crew_table, area); } @@ -383,11 +365,6 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { }; let current_route = app.get_current_route(); let mut default_movie_details_modal = MovieDetailsModal::default(); - let help_footer = app - .data - .radarr_data - .movie_info_tabs - .get_active_tab_contextual_help(); let content = Some( &mut app .data @@ -464,7 +441,6 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let releases_table = ManagarrTable::new(content, releases_row_mapping) .block(layout_block_top_border()) .loading(app.is_loading || is_empty) - .footer(help_footer) .sorting(active_radarr_block == ActiveRadarrBlock::ManualSearchSortPrompt) .headers([ "Source", "Age", "⛔", "Title", "Indexer", "Size", "Peers", "Language", "Quality", diff --git a/src/ui/radarr_ui/root_folders/mod.rs b/src/ui/radarr_ui/root_folders/mod.rs index 52655ee..f2361e1 100644 --- a/src/ui/radarr_ui/root_folders/mod.rs +++ b/src/ui/radarr_ui/root_folders/mod.rs @@ -59,11 +59,6 @@ impl DrawUi for RootFoldersUi { } fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let help_footer = app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help(); let root_folders_row_mapping = |root_folders: &RootFolder| { let RootFolder { path, @@ -94,7 +89,6 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(help_footer) .headers(["Path", "Free Space", "Unmapped Folders"]) .constraints([ Constraint::Ratio(3, 5), diff --git a/src/ui/radarr_ui/system/mod.rs b/src/ui/radarr_ui/system/mod.rs index 6d90f65..715197f 100644 --- a/src/ui/radarr_ui/system/mod.rs +++ b/src/ui/radarr_ui/system/mod.rs @@ -4,7 +4,7 @@ use chrono::Utc; use ratatui::layout::Layout; use ratatui::style::Style; use ratatui::text::{Span, Text}; -use ratatui::widgets::{Cell, Paragraph, Row}; +use ratatui::widgets::{Cell, Row}; use ratatui::{ layout::{Constraint, Rect}, widgets::ListItem, @@ -17,9 +17,7 @@ use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_models::QueueEvent; use crate::ui::radarr_ui::system::system_details_ui::SystemDetailsUi; use crate::ui::styles::ManagarrStyle; -use crate::ui::utils::{ - convert_to_minutes_hours_days, layout_block_top_border, style_log_list_item, -}; +use crate::ui::utils::{convert_to_minutes_hours_days, style_log_list_item}; use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::selectable_list::SelectableList; @@ -72,12 +70,8 @@ impl DrawUi for SystemUi { } fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let [activities_area, logs_area, help_area] = Layout::vertical([ - Constraint::Ratio(1, 2), - Constraint::Ratio(1, 2), - Constraint::Min(2), - ]) - .areas(area); + let [activities_area, logs_area] = + Layout::vertical([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(area); let [tasks_area, events_area] = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(activities_area); @@ -85,7 +79,6 @@ fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { draw_tasks(f, app, tasks_area); draw_queued_events(f, app, events_area); draw_logs(f, app, logs_area); - draw_help(f, app, help_area); } fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { @@ -188,26 +181,6 @@ fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f.render_widget(logs_box, area); } -fn draw_help(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let help_text = Text::from( - format!( - " {}", - app - .data - .radarr_data - .main_tabs - .get_active_tab_contextual_help() - .unwrap() - ) - .help(), - ); - let help_paragraph = Paragraph::new(help_text) - .block(layout_block_top_border()) - .left_aligned(); - - f.render_widget(help_paragraph, area); -} - pub(super) struct TaskProps { pub(super) name: String, pub(super) interval: String, diff --git a/src/ui/radarr_ui/system/system_details_ui.rs b/src/ui/radarr_ui/system/system_details_ui.rs index bf29a86..db1e538 100644 --- a/src/ui/radarr_ui/system/system_details_ui.rs +++ b/src/ui/radarr_ui/system/system_details_ui.rs @@ -3,8 +3,6 @@ use ratatui::text::{Span, Text}; use ratatui::widgets::{Cell, ListItem, Paragraph, Row}; use ratatui::Frame; -use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES}; -use crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES; use crate::app::App; use crate::models::radarr_models::RadarrTask; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS}; @@ -59,17 +57,10 @@ impl DrawUi for SystemDetailsUi { fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) { let block = title_block("Log Details"); - let help_footer = format!( - "<↑↓←→> scroll | {}", - build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) - ); if app.data.radarr_data.log_details.items.is_empty() { let loading = LoadingBlock::new(app.is_loading, borderless_block()); - let popup = Popup::new(loading) - .size(Size::Large) - .block(block) - .footer(&help_footer); + let popup = Popup::new(loading).size(Size::Large).block(block); f.render_widget(popup, f.area()); return; @@ -82,16 +73,12 @@ fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) { style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) }) .block(borderless_block()); - let popup = Popup::new(logs_list) - .size(Size::Large) - .block(block) - .footer(&help_footer); + let popup = Popup::new(logs_list).size(Size::Large).block(block); f.render_widget(popup, f.area()); } fn draw_tasks_popup(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: &RadarrTask| { let task_props = extract_task_props(task); @@ -107,7 +94,6 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping) .loading(app.is_loading) .margin(1) - .footer(help_footer) .footer_alignment(Alignment::Center) .headers(TASK_TABLE_HEADERS) .constraints(TASK_TABLE_CONSTRAINTS); @@ -136,10 +122,6 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>) { - let help_footer = format!( - "<↑↓> scroll | {}", - build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) - ); let updates = app.data.radarr_data.updates.get_text(); let block = title_block("Updates"); @@ -147,18 +129,12 @@ fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>) { let updates_paragraph = Paragraph::new(Text::from(updates)) .block(borderless_block()) .scroll((app.data.radarr_data.updates.offset, 0)); - let popup = Popup::new(updates_paragraph) - .size(Size::Large) - .block(block) - .footer(&help_footer); + let popup = Popup::new(updates_paragraph).size(Size::Large).block(block); f.render_widget(popup, f.area()); } else { let loading = LoadingBlock::new(app.is_loading, borderless_block()); - let popup = Popup::new(loading) - .size(Size::Large) - .block(block) - .footer(&help_footer); + let popup = Popup::new(loading).size(Size::Large).block(block); f.render_widget(popup, f.area()); } diff --git a/src/ui/sonarr_ui/blocklist/mod.rs b/src/ui/sonarr_ui/blocklist/mod.rs index 442c98a..e1bb0f9 100644 --- a/src/ui/sonarr_ui/blocklist/mod.rs +++ b/src/ui/sonarr_ui/blocklist/mod.rs @@ -77,12 +77,6 @@ impl DrawUi for BlocklistUi { fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { - let blocklist_table_footer = app - .data - .sonarr_data - .main_tabs - .get_active_tab_contextual_help(); - let blocklist_row_mapping = |blocklist_item: &BlocklistItem| { let BlocklistItem { source_title, @@ -115,7 +109,6 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(blocklist_table_footer) .sorting(active_sonarr_block == ActiveSonarrBlock::BlocklistSortPrompt) .headers([ "Series Title", diff --git a/src/ui/sonarr_ui/downloads/mod.rs b/src/ui/sonarr_ui/downloads/mod.rs index ba3238f..01362b1 100644 --- a/src/ui/sonarr_ui/downloads/mod.rs +++ b/src/ui/sonarr_ui/downloads/mod.rs @@ -72,11 +72,6 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { app.data.sonarr_data.downloads.current_selection().clone() }; - let downloads_table_footer = app - .data - .sonarr_data - .main_tabs - .get_active_tab_contextual_help(); let downloads_row_mapping = |download_record: &DownloadRecord| { let DownloadRecord { @@ -130,7 +125,6 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(downloads_table_footer) .headers([ "Title", "Percent Complete", diff --git a/src/ui/sonarr_ui/history/mod.rs b/src/ui/sonarr_ui/history/mod.rs index 268bfdc..ea85300 100644 --- a/src/ui/sonarr_ui/history/mod.rs +++ b/src/ui/sonarr_ui/history/mod.rs @@ -55,12 +55,6 @@ fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { app.data.sonarr_data.history.current_selection().clone() }; if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { - let history_table_footer = app - .data - .sonarr_data - .main_tabs - .get_active_tab_contextual_help(); - let history_row_mapping = |history_item: &SonarrHistoryItem| { let SonarrHistoryItem { source_title, @@ -96,7 +90,6 @@ fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ManagarrTable::new(Some(&mut app.data.sonarr_data.history), history_row_mapping) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(history_table_footer) .sorting(active_sonarr_block == ActiveSonarrBlock::HistorySortPrompt) .searching(active_sonarr_block == ActiveSonarrBlock::SearchHistory) .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchHistoryError) diff --git a/src/ui/sonarr_ui/indexers/edit_indexer_ui.rs b/src/ui/sonarr_ui/indexers/edit_indexer_ui.rs index 5606eeb..58d716f 100644 --- a/src/ui/sonarr_ui/indexers/edit_indexer_ui.rs +++ b/src/ui/sonarr_ui/indexers/edit_indexer_ui.rs @@ -1,6 +1,5 @@ use std::sync::atomic::Ordering; -use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS}; use crate::models::Route; @@ -14,8 +13,6 @@ use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::popup::Size; use crate::ui::{draw_popup, DrawUi}; use ratatui::layout::{Constraint, Flex, Layout, Rect}; -use ratatui::text::Text; -use ratatui::widgets::Paragraph; use ratatui::Frame; #[cfg(test)] @@ -45,22 +42,15 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let highlight_yes_no = selected_block == ActiveSonarrBlock::EditIndexerConfirmPrompt; let edit_indexer_modal_option = &app.data.sonarr_data.edit_indexer_modal; let protocol = &app.data.sonarr_data.indexers.current_selection().protocol; - let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text).centered(); if edit_indexer_modal_option.is_some() { f.render_widget(block, area); let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap(); - let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([ - Constraint::Fill(1), - Constraint::Length(18), - Constraint::Fill(1), - Constraint::Length(3), - Constraint::Length(1), - ]) - .margin(1) - .areas(area); + let [settings_area, buttons_area] = + Layout::vertical([Constraint::Fill(1), Constraint::Length(3)]) + .margin(1) + .areas(area); let [left_side_area, right_side_area] = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) .margin(1) @@ -169,7 +159,6 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f.render_widget(interactive_search_checkbox, interactive_search_area); f.render_widget(save_button, save_area); f.render_widget(cancel_button, cancel_area); - f.render_widget(help_paragraph, help_area); } } else { f.render_widget(LoadingBlock::new(app.is_loading, block), area); diff --git a/src/ui/sonarr_ui/indexers/indexer_settings_ui.rs b/src/ui/sonarr_ui/indexers/indexer_settings_ui.rs index 969a799..3e01503 100644 --- a/src/ui/sonarr_ui/indexers/indexer_settings_ui.rs +++ b/src/ui/sonarr_ui/indexers/indexer_settings_ui.rs @@ -1,9 +1,6 @@ use ratatui::layout::{Constraint, Flex, Layout, Rect}; -use ratatui::text::Text; -use ratatui::widgets::Paragraph; use ratatui::Frame; -use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, @@ -44,14 +41,12 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: let selected_block = app.data.sonarr_data.selected_block.get_active_block(); let highlight_yes_no = selected_block == ActiveSonarrBlock::IndexerSettingsConfirmPrompt; let indexer_settings_option = &app.data.sonarr_data.indexer_settings; - let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text).centered(); if indexer_settings_option.is_some() { f.render_widget(block, area); let indexer_settings = indexer_settings_option.as_ref().unwrap(); - let [_, min_age_area, retention_area, max_size_area, rss_sync_area, _, buttons_area, help_area] = + let [_, min_age_area, retention_area, max_size_area, rss_sync_area, _, buttons_area] = Layout::vertical([ Constraint::Fill(1), Constraint::Length(3), @@ -60,7 +55,6 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Constraint::Length(3), Constraint::Fill(1), Constraint::Length(3), - Constraint::Length(1), ]) .margin(1) .areas(area); @@ -112,7 +106,6 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: f.render_widget(save_button, save_area); f.render_widget(cancel_button, cancel_area); - f.render_widget(help_paragraph, help_area); } else { f.render_widget(LoadingBlock::new(app.is_loading, block), area); } diff --git a/src/ui/sonarr_ui/indexers/mod.rs b/src/ui/sonarr_ui/indexers/mod.rs index a8ae619..fa7e3f4 100644 --- a/src/ui/sonarr_ui/indexers/mod.rs +++ b/src/ui/sonarr_ui/indexers/mod.rs @@ -157,17 +157,11 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let indexers_table_footer = app - .data - .sonarr_data - .main_tabs - .get_active_tab_contextual_help(); let indexers_table = ManagarrTable::new( Some(&mut app.data.sonarr_data.indexers), indexers_row_mapping, ) .block(layout_block_top_border()) - .footer(indexers_table_footer) .loading(app.is_loading) .headers([ "Indexer", diff --git a/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs index 6876329..e6c5c05 100644 --- a/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs +++ b/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs @@ -1,4 +1,3 @@ -use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; @@ -41,10 +40,6 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are IndexerTestResultModalItem::default() }; f.render_widget(title_block("Test All Indexers"), area); - let help_footer = format!( - "<↑↓> scroll | {}", - build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) - ); let test_results_row_mapping = |result: &IndexerTestResultModalItem| { result.validation_failures.scroll_left_or_reset( get_width_from_percentage(area, 86), @@ -70,7 +65,6 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are test_results_row_mapping, ) .loading(is_loading) - .footer(Some(help_footer)) .footer_alignment(Alignment::Center) .margin(1) .headers(["Indexer", "Pass/Fail", "Failure Messages"]) diff --git a/src/ui/sonarr_ui/library/add_series_ui.rs b/src/ui/sonarr_ui/library/add_series_ui.rs index 40335ae..2754ad7 100644 --- a/src/ui/sonarr_ui/library/add_series_ui.rs +++ b/src/ui/sonarr_ui/library/add_series_ui.rs @@ -1,22 +1,16 @@ use std::sync::atomic::Ordering; use ratatui::layout::{Constraint, Layout, Rect}; -use ratatui::text::Text; -use ratatui::widgets::{Cell, ListItem, Paragraph, Row}; +use ratatui::widgets::{Cell, ListItem, Row}; use ratatui::Frame; -use crate::app::context_clues::{ - build_context_clue_string, BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, -}; -use crate::app::sonarr::sonarr_context_clues::ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES; use crate::models::servarr_data::sonarr::modals::AddSeriesModal; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS}; use crate::models::sonarr_models::AddSeriesSearchResult; use crate::models::Route; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ - borderless_block, get_width_from_percentage, layout_block, layout_paragraph_borderless, - title_block_centered, + get_width_from_percentage, layout_block, layout_paragraph_borderless, title_block_centered, }; use crate::ui::widgets::button::Button; use crate::ui::widgets::checkbox::Checkbox; @@ -78,13 +72,10 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { AddSeriesSearchResult::default() }; - let [search_box_area, results_area, help_area] = Layout::vertical([ - Constraint::Length(3), - Constraint::Fill(0), - Constraint::Length(3), - ]) - .margin(1) - .areas(area); + let [search_box_area, results_area] = + Layout::vertical([Constraint::Length(3), Constraint::Fill(0)]) + .margin(1) + .areas(area); let block_content = &app .data .sonarr_data @@ -150,27 +141,17 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let search_box = InputBox::new(block_content) .offset(offset) .block(title_block_centered("Add Series")); - let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text) - .block(borderless_block()) - .centered(); search_box.show_cursor(f, search_box_area); f.render_widget(layout_block().default(), results_area); f.render_widget(search_box, search_box_area); - f.render_widget(help_paragraph, help_area); } ActiveSonarrBlock::AddSeriesEmptySearchResults => { - let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text) - .block(borderless_block()) - .centered(); let error_message = Message::new("No series found matching your query!"); let error_message_popup = Popup::new(error_message).size(Size::Message); f.render_widget(layout_block().default(), results_area); f.render_widget(error_message_popup, f.area()); - f.render_widget(help_paragraph, help_area); } ActiveSonarrBlock::AddSeriesSearchResults | ActiveSonarrBlock::AddSeriesPrompt @@ -181,11 +162,6 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { | ActiveSonarrBlock::AddSeriesSelectRootFolder | ActiveSonarrBlock::AddSeriesAlreadyInLibrary | ActiveSonarrBlock::AddSeriesTagsInput => { - let help_text = - Text::from(build_context_clue_string(&ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text) - .block(borderless_block()) - .centered(); let search_results_table = ManagarrTable::new( app.data.sonarr_data.add_searched_series.as_mut(), search_results_row_mapping, @@ -206,7 +182,6 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]); f.render_widget(search_results_table, results_area); - f.render_widget(help_paragraph, help_area); } _ => (), } @@ -297,7 +272,7 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f.render_widget(title_block_centered(&title), area); - let [paragraph_area, root_folder_area, monitor_area, quality_profile_area, language_profile_area, series_type_area, season_folder_area, tags_area, _, buttons_area, help_area] = + let [paragraph_area, root_folder_area, monitor_area, quality_profile_area, language_profile_area, series_type_area, season_folder_area, tags_area, _, buttons_area] = Layout::vertical([ Constraint::Length(6), Constraint::Length(3), @@ -309,16 +284,12 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Constraint::Length(3), Constraint::Fill(1), Constraint::Length(3), - Constraint::Length(1), ]) .margin(1) .areas(area); let prompt_paragraph = layout_paragraph_borderless(&prompt); - let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text).centered(); f.render_widget(prompt_paragraph, paragraph_area); - f.render_widget(help_paragraph, help_area); let [add_area, cancel_area] = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) diff --git a/src/ui/sonarr_ui/library/edit_series_ui.rs b/src/ui/sonarr_ui/library/edit_series_ui.rs index 5ea60e5..9307235 100644 --- a/src/ui/sonarr_ui/library/edit_series_ui.rs +++ b/src/ui/sonarr_ui/library/edit_series_ui.rs @@ -2,11 +2,9 @@ use std::sync::atomic::Ordering; use ratatui::layout::{Constraint, Rect}; use ratatui::prelude::Layout; -use ratatui::text::Text; -use ratatui::widgets::{ListItem, Paragraph}; +use ratatui::widgets::ListItem; use ratatui::Frame; -use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::app::App; use crate::models::servarr_data::sonarr::modals::EditSeriesModal; use crate::models::servarr_data::sonarr::sonarr_data::{ @@ -107,7 +105,7 @@ fn draw_edit_series_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, ar let selected_quality_profile = quality_profile_list.current_selection(); let selected_language_profile = language_profile_list.current_selection(); - let [paragraph_area, monitored_area, season_folder_area, quality_profile_area, language_profile_area, series_type_area, path_area, tags_area, _, buttons_area, help_area] = + let [paragraph_area, monitored_area, season_folder_area, quality_profile_area, language_profile_area, series_type_area, path_area, tags_area, _, buttons_area] = Layout::vertical([ Constraint::Length(6), Constraint::Length(3), @@ -119,7 +117,6 @@ fn draw_edit_series_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, ar Constraint::Length(3), Constraint::Fill(1), Constraint::Length(3), - Constraint::Length(1), ]) .margin(1) .areas(area); @@ -127,8 +124,6 @@ fn draw_edit_series_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, ar Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) .areas(buttons_area); - let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text).centered(); let prompt_paragraph = layout_paragraph_borderless(&series_overview); let monitored_checkbox = Checkbox::new("Monitored") .checked(monitored.unwrap_or_default()) @@ -189,7 +184,6 @@ fn draw_edit_series_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, ar f.render_widget(series_type_drop_down_button, series_type_area); f.render_widget(save_button, save_area); f.render_widget(cancel_button, cancel_area); - f.render_widget(help_paragraph, help_area); } fn draw_edit_series_select_series_type_popup(f: &mut Frame<'_>, app: &mut App<'_>) { diff --git a/src/ui/sonarr_ui/library/episode_details_ui.rs b/src/ui/sonarr_ui/library/episode_details_ui.rs index 99b1849..786edf7 100644 --- a/src/ui/sonarr_ui/library/episode_details_ui.rs +++ b/src/ui/sonarr_ui/library/episode_details_ui.rs @@ -257,9 +257,6 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) .current_selection() .clone() }; - let episode_history_table_footer = episode_details_modal - .episode_details_tabs - .get_active_tab_contextual_help(); let history_row_mapping = |history_item: &SonarrHistoryItem| { let SonarrHistoryItem { @@ -306,7 +303,6 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) ManagarrTable::new(Some(&mut episode_history_table), history_row_mapping) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(episode_history_table_footer) .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) .constraints([ Constraint::Percentage(40), @@ -397,9 +393,6 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { episode_details_modal.episode_releases.is_empty(), ) }; - let episode_release_table_footer = episode_details_modal - .episode_details_tabs - .get_active_tab_contextual_help(); if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { let episode_release_row_mapping = |release: &SonarrRelease| { @@ -483,7 +476,6 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ) .block(layout_block_top_border()) .loading(app.is_loading || is_empty) - .footer(episode_release_table_footer) .sorting(active_sonarr_block == ActiveSonarrBlock::ManualEpisodeSearchSortPrompt) .headers([ "Source", "Age", "⛔", "Title", "Indexer", "Size", "Peers", "Language", "Quality", diff --git a/src/ui/sonarr_ui/library/mod.rs b/src/ui/sonarr_ui/library/mod.rs index d583c0b..fa2d6da 100644 --- a/src/ui/sonarr_ui/library/mod.rs +++ b/src/ui/sonarr_ui/library/mod.rs @@ -90,11 +90,6 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let language_profile_map = &app.data.sonarr_data.language_profiles_map; let tags_map = &app.data.sonarr_data.tags_map; let content = Some(&mut app.data.sonarr_data.series); - let help_footer = app - .data - .sonarr_data - .main_tabs - .get_active_tab_contextual_help(); let series_table_row_mapping = |series: &Series| { series.title.scroll_left_or_reset( @@ -154,7 +149,6 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let series_table = ManagarrTable::new(content, series_table_row_mapping) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(help_footer) .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesSortPrompt) .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeries) .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeries) diff --git a/src/ui/sonarr_ui/library/season_details_ui.rs b/src/ui/sonarr_ui/library/season_details_ui.rs index 35eccc6..6a8ca36 100644 --- a/src/ui/sonarr_ui/library/season_details_ui.rs +++ b/src/ui/sonarr_ui/library/season_details_ui.rs @@ -149,14 +149,6 @@ pub fn draw_season_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_episodes_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { - let help_footer = app - .data - .sonarr_data - .season_details_modal - .as_ref() - .expect("Season details modal is unpopulated") - .season_details_tabs - .get_active_tab_contextual_help(); let episode_files = app .data .sonarr_data @@ -222,7 +214,6 @@ fn draw_episodes_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let season_table = ManagarrTable::new(content, episode_row_mapping) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(help_footer) .searching(is_searching) .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchEpisodesError) .headers([ @@ -261,9 +252,6 @@ fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .current_selection() .clone() }; - let season_history_table_footer = season_details_modal - .season_details_tabs - .get_active_tab_contextual_help(); if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { let history_row_mapping = |history_item: &SonarrHistoryItem| { @@ -308,7 +296,6 @@ fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ManagarrTable::new(Some(&mut season_history_table), history_row_mapping) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(season_history_table_footer) .sorting(active_sonarr_block == ActiveSonarrBlock::SeasonHistorySortPrompt) .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeasonHistory) .search_produced_empty_results( @@ -363,9 +350,6 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { season_details_modal.season_releases.is_empty(), ) }; - let season_release_table_footer = season_details_modal - .season_details_tabs - .get_active_tab_contextual_help(); if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { let season_release_row_mapping = |release: &SonarrRelease| { @@ -444,7 +428,6 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ManagarrTable::new(Some(&mut season_release_table), season_release_row_mapping) .block(layout_block_top_border()) .loading(app.is_loading || is_empty) - .footer(season_release_table_footer) .sorting(active_sonarr_block == ActiveSonarrBlock::ManualSeasonSearchSortPrompt) .headers([ "Source", "Age", "⛔", "Title", "Indexer", "Size", "Peers", "Language", "Quality", diff --git a/src/ui/sonarr_ui/library/series_details_ui.rs b/src/ui/sonarr_ui/library/series_details_ui.rs index 7054310..10e286d 100644 --- a/src/ui/sonarr_ui/library/series_details_ui.rs +++ b/src/ui/sonarr_ui/library/series_details_ui.rs @@ -229,11 +229,6 @@ pub fn draw_series_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { let content = Some(&mut app.data.sonarr_data.seasons); - let help_footer = app - .data - .sonarr_data - .series_info_tabs - .get_active_tab_contextual_help(); let season_row_mapping = |season: &Season| { let Season { title, @@ -279,7 +274,6 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let season_table = ManagarrTable::new(content, season_row_mapping) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(help_footer) .searching(is_searching) .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeasonError) .headers(["Monitored", "Season", "Episode Count", "Size on Disk"]) @@ -306,11 +300,6 @@ fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { series_history.current_selection().clone() }; - let series_history_table_footer = app - .data - .sonarr_data - .series_info_tabs - .get_active_tab_contextual_help(); if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { let history_row_mapping = |history_item: &SonarrHistoryItem| { @@ -349,7 +338,6 @@ fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ManagarrTable::new(Some(&mut series_history_table), history_row_mapping) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(series_history_table_footer) .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesHistorySortPrompt) .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeriesHistory) .search_produced_empty_results( diff --git a/src/ui/sonarr_ui/root_folders/mod.rs b/src/ui/sonarr_ui/root_folders/mod.rs index a8f7495..1d2728e 100644 --- a/src/ui/sonarr_ui/root_folders/mod.rs +++ b/src/ui/sonarr_ui/root_folders/mod.rs @@ -59,11 +59,6 @@ impl DrawUi for RootFoldersUi { } fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let help_footer = app - .data - .sonarr_data - .main_tabs - .get_active_tab_contextual_help(); let root_folders_row_mapping = |root_folders: &RootFolder| { let RootFolder { path, @@ -94,7 +89,6 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ) .block(layout_block_top_border()) .loading(app.is_loading) - .footer(help_footer) .headers(["Path", "Free Space", "Unmapped Folders"]) .constraints([ Constraint::Ratio(3, 5), diff --git a/src/ui/sonarr_ui/system/mod.rs b/src/ui/sonarr_ui/system/mod.rs index f6c3824..d4fd709 100644 --- a/src/ui/sonarr_ui/system/mod.rs +++ b/src/ui/sonarr_ui/system/mod.rs @@ -4,7 +4,7 @@ use chrono::Utc; use ratatui::layout::Layout; use ratatui::style::Style; use ratatui::text::{Span, Text}; -use ratatui::widgets::{Cell, Paragraph, Row}; +use ratatui::widgets::{Cell, Row}; use ratatui::{ layout::{Constraint, Rect}, widgets::ListItem, @@ -17,9 +17,7 @@ use crate::models::servarr_models::QueueEvent; use crate::models::sonarr_models::SonarrTask; use crate::ui::sonarr_ui::system::system_details_ui::SystemDetailsUi; use crate::ui::styles::ManagarrStyle; -use crate::ui::utils::{ - convert_to_minutes_hours_days, layout_block_top_border, style_log_list_item, -}; +use crate::ui::utils::{convert_to_minutes_hours_days, style_log_list_item}; use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::selectable_list::SelectableList; @@ -66,12 +64,8 @@ impl DrawUi for SystemUi { } fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let [activities_area, logs_area, help_area] = Layout::vertical([ - Constraint::Ratio(1, 2), - Constraint::Ratio(1, 2), - Constraint::Min(2), - ]) - .areas(area); + let [activities_area, logs_area] = + Layout::vertical([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(area); let [tasks_area, events_area] = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(activities_area); @@ -79,7 +73,6 @@ fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { draw_tasks(f, app, tasks_area); draw_queued_events(f, app, events_area); draw_logs(f, app, logs_area); - draw_help(f, app, help_area); } fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { @@ -181,26 +174,6 @@ fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { f.render_widget(logs_box, area); } -fn draw_help(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let help_text = Text::from( - format!( - " {}", - app - .data - .sonarr_data - .main_tabs - .get_active_tab_contextual_help() - .unwrap() - ) - .help(), - ); - let help_paragraph = Paragraph::new(help_text) - .block(layout_block_top_border()) - .left_aligned(); - - f.render_widget(help_paragraph, area); -} - pub(super) struct TaskProps { pub(super) name: String, pub(super) interval: String, diff --git a/src/ui/sonarr_ui/system/system_details_ui.rs b/src/ui/sonarr_ui/system/system_details_ui.rs index bc12a21..45cad02 100644 --- a/src/ui/sonarr_ui/system/system_details_ui.rs +++ b/src/ui/sonarr_ui/system/system_details_ui.rs @@ -3,8 +3,6 @@ use ratatui::text::{Span, Text}; use ratatui::widgets::{Cell, ListItem, Paragraph, Row}; use ratatui::Frame; -use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES}; -use crate::app::sonarr::sonarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES; use crate::app::App; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS}; use crate::models::sonarr_models::SonarrTask; @@ -59,17 +57,10 @@ impl DrawUi for SystemDetailsUi { fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) { let block = title_block("Log Details"); - let help_footer = format!( - "<↑↓←→> scroll | {}", - build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) - ); if app.data.sonarr_data.log_details.items.is_empty() { let loading = LoadingBlock::new(app.is_loading, borderless_block()); - let popup = Popup::new(loading) - .size(Size::Large) - .block(block) - .footer(&help_footer); + let popup = Popup::new(loading).size(Size::Large).block(block); f.render_widget(popup, f.area()); return; @@ -82,16 +73,12 @@ fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) { style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) }) .block(borderless_block()); - let popup = Popup::new(logs_list) - .size(Size::Large) - .block(block) - .footer(&help_footer); + let popup = Popup::new(logs_list).size(Size::Large).block(block); f.render_widget(popup, f.area()); } fn draw_tasks_popup(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: &SonarrTask| { let task_props = extract_task_props(task); @@ -106,7 +93,6 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let tasks_table = ManagarrTable::new(Some(&mut app.data.sonarr_data.tasks), tasks_row_mapping) .loading(app.is_loading) .margin(1) - .footer(help_footer) .footer_alignment(Alignment::Center) .headers(TASK_TABLE_HEADERS) .constraints(TASK_TABLE_CONSTRAINTS); @@ -135,10 +121,6 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>) { - let help_footer = format!( - "<↑↓> scroll | {}", - build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES) - ); let updates = app.data.sonarr_data.updates.get_text(); let block = title_block("Updates"); @@ -146,18 +128,12 @@ fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>) { let updates_paragraph = Paragraph::new(Text::from(updates)) .block(borderless_block()) .scroll((app.data.sonarr_data.updates.offset, 0)); - let popup = Popup::new(updates_paragraph) - .size(Size::Large) - .block(block) - .footer(&help_footer); + let popup = Popup::new(updates_paragraph).size(Size::Large).block(block); f.render_widget(popup, f.area()); } else { let loading = LoadingBlock::new(app.is_loading, borderless_block()); - let popup = Popup::new(loading) - .size(Size::Large) - .block(block) - .footer(&help_footer); + let popup = Popup::new(loading).size(Size::Large).block(block); f.render_widget(popup, f.area()); } diff --git a/src/ui/widgets/confirmation_prompt.rs b/src/ui/widgets/confirmation_prompt.rs index bba0c6e..7f926b0 100644 --- a/src/ui/widgets/confirmation_prompt.rs +++ b/src/ui/widgets/confirmation_prompt.rs @@ -1,4 +1,3 @@ -use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES}; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{layout_paragraph_borderless, title_block_centered}; use crate::ui::widgets::button::Button; @@ -6,7 +5,6 @@ use crate::ui::widgets::checkbox::Checkbox; use derive_setters::Setters; use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Flex, Layout, Rect}; -use ratatui::text::Text; use ratatui::widgets::{Paragraph, Widget}; use std::iter; @@ -40,16 +38,12 @@ impl ConfirmationPrompt<'_> { fn render_confirmation_prompt_with_checkboxes(self, area: Rect, buf: &mut Buffer) { title_block_centered(self.title).render(area, buf); - let help_text = - Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text).centered(); if let Some(checkboxes) = self.checkboxes { let mut constraints = vec![ Constraint::Length(4), Constraint::Fill(1), Constraint::Length(3), - Constraint::Length(1), ]; constraints.splice( 1..1, @@ -61,7 +55,6 @@ impl ConfirmationPrompt<'_> { .areas(chunks[checkboxes.len() + 2]); layout_paragraph_borderless(self.prompt).render(chunks[0], buf); - help_paragraph.render(chunks[checkboxes.len() + 3], buf); checkboxes .into_iter() @@ -83,38 +76,30 @@ impl ConfirmationPrompt<'_> { fn render_confirmation_prompt(self, area: Rect, buf: &mut Buffer) { title_block_centered(self.title).render(area, buf); - let help_text = - Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help()); - let help_paragraph = Paragraph::new(help_text).centered(); let [prompt_area, buttons_area] = if let Some(content_paragraph) = self.content { - let [prompt_area, content_area, _, buttons_area, help_area] = Layout::vertical([ + let [prompt_area, content_area, _, buttons_area] = Layout::vertical([ Constraint::Length(4), Constraint::Length(7), Constraint::Fill(1), Constraint::Length(3), - Constraint::Length(1), ]) .margin(1) .areas(area); content_paragraph.render(content_area, buf); - help_paragraph.render(help_area, buf); [prompt_area, buttons_area] } else { - let [prompt_area, buttons_area, _, help_area] = Layout::vertical([ + let [prompt_area, _, buttons_area] = Layout::vertical([ Constraint::Percentage(72), - Constraint::Length(3), Constraint::Fill(0), - Constraint::Min(1), + Constraint::Length(3), ]) .margin(1) .flex(Flex::SpaceBetween) .areas(area); - help_paragraph.render(help_area, buf); - [prompt_area, buttons_area] }; diff --git a/src/ui/widgets/input_box_popup.rs b/src/ui/widgets/input_box_popup.rs index 9edd27b..63849fa 100644 --- a/src/ui/widgets/input_box_popup.rs +++ b/src/ui/widgets/input_box_popup.rs @@ -1,8 +1,7 @@ -use crate::ui::styles::ManagarrStyle; -use crate::ui::utils::{background_block, borderless_block, centered_rect}; +use crate::ui::utils::{background_block, centered_rect}; use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Layout, Rect}; -use ratatui::widgets::{Block, Clear, Paragraph, Widget, WidgetRef}; +use ratatui::widgets::{Block, Clear, Widget, WidgetRef}; use super::input_box::InputBox; @@ -39,17 +38,10 @@ impl<'a> InputBoxPopup<'a> { Clear.render(popup_area, buf); background_block().render(popup_area, buf); - let [text_box_area, help_area] = - Layout::vertical([Constraint::Length(3), Constraint::Length(1)]) - .margin(1) - .areas(popup_area); + let [text_box_area] = Layout::vertical([Constraint::Length(3)]) + .margin(1) + .areas(popup_area); self.input_box.render_ref(text_box_area, buf); - - let help = Paragraph::new(" cancel") - .help() - .centered() - .block(borderless_block()); - help.render(help_area, buf); } } diff --git a/src/ui/widgets/popup.rs b/src/ui/widgets/popup.rs index 7c7cddb..0534b95 100644 --- a/src/ui/widgets/popup.rs +++ b/src/ui/widgets/popup.rs @@ -1,9 +1,7 @@ -use crate::ui::styles::ManagarrStyle; -use crate::ui::utils::{background_block, centered_rect, layout_block_top_border}; +use crate::ui::utils::{background_block, centered_rect}; use ratatui::buffer::Buffer; -use ratatui::layout::{Constraint, Layout, Rect}; -use ratatui::prelude::Text; -use ratatui::widgets::{Block, Clear, Paragraph, Widget}; +use ratatui::layout::Rect; +use ratatui::widgets::{Block, Clear, Widget}; #[cfg(test)] #[path = "popup_tests.rs"] @@ -25,6 +23,7 @@ pub enum Size { XLarge, XXLarge, Long, + LongNarrowTable, } impl Size { @@ -45,6 +44,7 @@ impl Size { Size::XLarge => (83, 83), Size::XXLarge => (90, 90), Size::Long => (65, 75), + Size::LongNarrowTable => (55, 85), } } } @@ -54,7 +54,6 @@ pub struct Popup<'a, T: Widget> { percent_x: u16, percent_y: u16, block: Option>, - footer: Option<&'a str>, } impl<'a, T: Widget> Popup<'a, T> { @@ -64,7 +63,6 @@ impl<'a, T: Widget> Popup<'a, T> { percent_x: 0, percent_y: 0, block: None, - footer: None, } } @@ -86,11 +84,6 @@ impl<'a, T: Widget> Popup<'a, T> { self } - pub fn footer(mut self, footer: &'a str) -> Self { - self.footer = Some(footer); - self - } - fn render_popup(self, area: Rect, buf: &mut Buffer) { let mut popup_area = centered_rect(self.percent_x, self.percent_y, area); let height = if popup_area.height < 3 { @@ -109,23 +102,7 @@ impl<'a, T: Widget> Popup<'a, T> { block.render(popup_area, buf); } - let content_area = if let Some(footer) = self.footer { - let [content_area, help_footer_area] = - Layout::vertical([Constraint::Fill(0), Constraint::Length(2)]) - .margin(1) - .areas(popup_area); - - Paragraph::new(Text::from(format!(" {footer}").help())) - .block(layout_block_top_border()) - .left_aligned() - .render(help_footer_area, buf); - - content_area - } else { - popup_area - }; - - self.widget.render(content_area, buf); + self.widget.render(popup_area, buf); } } diff --git a/src/ui/widgets/popup_tests.rs b/src/ui/widgets/popup_tests.rs index 9f82a44..3c0fedb 100644 --- a/src/ui/widgets/popup_tests.rs +++ b/src/ui/widgets/popup_tests.rs @@ -21,6 +21,7 @@ mod tests { assert_eq!(Size::XLarge.to_percent(), (83, 83)); assert_eq!(Size::XXLarge.to_percent(), (90, 90)); assert_eq!(Size::Long.to_percent(), (65, 75)); + assert_eq!(Size::LongNarrowTable.to_percent(), (55, 85)); } #[test] @@ -31,7 +32,6 @@ mod tests { assert_eq!(popup.percent_x, 0); assert_eq!(popup.percent_y, 0); assert_eq!(popup.block, None); - assert_eq!(popup.footer, None); } #[test] @@ -42,7 +42,6 @@ mod tests { assert_eq!(popup.percent_y, 40); assert_eq!(popup.widget, Block::new()); assert_eq!(popup.block, None); - assert_eq!(popup.footer, None); } #[test] @@ -53,7 +52,6 @@ mod tests { assert_eq!(popup.percent_y, 50); assert_eq!(popup.widget, Block::new()); assert_eq!(popup.block, None); - assert_eq!(popup.footer, None); } #[test] @@ -64,17 +62,5 @@ mod tests { assert_eq!(popup.widget, Block::new()); assert_eq!(popup.percent_x, 0); assert_eq!(popup.percent_y, 0); - assert_eq!(popup.footer, None); - } - - #[test] - fn test_popup_footer() { - let popup = Popup::new(Block::new()).footer("footer"); - - assert_eq!(popup.footer, Some("footer")); - assert_eq!(popup.widget, Block::new()); - assert_eq!(popup.percent_x, 0); - assert_eq!(popup.percent_y, 0); - assert_eq!(popup.block, None); } }