feat: Refactor all keybinding tips into a dynamically changing menu that can be invoked via '?' [#32]

This commit is contained in:
2025-08-12 16:27:34 -06:00
parent 1f4870d082
commit 00ab0f27f7
64 changed files with 1627 additions and 903 deletions
-17
View File
@@ -5,7 +5,6 @@ mod tests {
use serial_test::serial; use serial_test::serial;
use tokio::sync::mpsc; 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::app::{interpolate_env_vars, App, AppConfig, Data, ServarrConfig};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
@@ -39,40 +38,24 @@ mod tests {
TabRoute { TabRoute {
title: "Sonarr Test".to_owned(), title: "Sonarr Test".to_owned(),
route: ActiveSonarrBlock::default().into(), route: ActiveSonarrBlock::default().into(),
help: format!(
"<↑↓> scroll | <C-u/d> page up/down | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None, contextual_help: None,
config: Some(sonarr_config_1), config: Some(sonarr_config_1),
}, },
TabRoute { TabRoute {
title: "Radarr 1".to_owned(), title: "Radarr 1".to_owned(),
route: ActiveRadarrBlock::default().into(), route: ActiveRadarrBlock::default().into(),
help: format!(
"<↑↓> scroll | <C-u/d> page up/down | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None, contextual_help: None,
config: Some(radarr_config_2), config: Some(radarr_config_2),
}, },
TabRoute { TabRoute {
title: "Radarr Test".to_owned(), title: "Radarr Test".to_owned(),
route: ActiveRadarrBlock::default().into(), route: ActiveRadarrBlock::default().into(),
help: format!(
"<↑↓> scroll | <C-u/d> page up/down | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None, contextual_help: None,
config: Some(radarr_config_1), config: Some(radarr_config_1),
}, },
TabRoute { TabRoute {
title: "Sonarr 1".to_owned(), title: "Sonarr 1".to_owned(),
route: ActiveSonarrBlock::default().into(), route: ActiveSonarrBlock::default().into(),
help: format!(
"<↑↓> scroll | <C-u/d> page up/down | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None, contextual_help: None,
config: Some(sonarr_config_2), config: Some(sonarr_config_2),
}, },
+34 -8
View File
@@ -1,25 +1,51 @@
use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS}; 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)] #[cfg(test)]
#[path = "context_clues_tests.rs"] #[path = "context_clues_tests.rs"]
mod context_clues_tests; 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 { pub trait ContextClueProvider {
context_clues fn get_context_clues(_app: &mut App<'_>) -> Option<&'static [ContextClue]>;
.iter()
.map(|(key_binding, desc)| format!("{} {desc}", key_binding.key))
.collect::<Vec<String>>()
.join(" | ")
} }
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,
DEFAULT_KEYBINDINGS.next_servarr.desc, DEFAULT_KEYBINDINGS.next_servarr.desc,
), ),
(
DEFAULT_KEYBINDINGS.previous_servarr,
DEFAULT_KEYBINDINGS.previous_servarr.desc,
),
(DEFAULT_KEYBINDINGS.quit, DEFAULT_KEYBINDINGS.quit.desc), (DEFAULT_KEYBINDINGS.quit, DEFAULT_KEYBINDINGS.quit.desc),
(DEFAULT_KEYBINDINGS.help, DEFAULT_KEYBINDINGS.help.desc),
]; ];
pub static BARE_POPUP_CONTEXT_CLUES: [ContextClue; 1] = pub static BARE_POPUP_CONTEXT_CLUES: [ContextClue; 1] =
+86 -17
View File
@@ -3,24 +3,15 @@ mod test {
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use crate::app::context_clues::{ use crate::app::context_clues::{
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, ContextClueProvider, ServarrContextClueProvider, BARE_POPUP_CONTEXT_CLUES,
DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES,
SERVARR_CONTEXT_CLUES, SYSTEM_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}; use crate::app::{key_binding::DEFAULT_KEYBINDINGS, App};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
#[test] use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
fn test_build_context_clue_string() { use crate::models::servarr_data::ActiveKeybindingBlock;
let test_context_clues_array = [
(DEFAULT_KEYBINDINGS.add, "add"),
(DEFAULT_KEYBINDINGS.delete, "delete"),
];
assert_str_eq!(
build_context_clue_string(&test_context_clues_array),
"<a> add | <del> delete"
);
}
#[test] #[test]
fn test_servarr_context_clues() { fn test_servarr_context_clues() {
@@ -28,13 +19,53 @@ mod test {
let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); 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_eq!(*key_binding, DEFAULT_KEYBINDINGS.next_servarr);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.next_servarr.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.next_servarr.desc);
let (key_binding, description) = servarr_context_clues_iter.next().unwrap(); 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_eq!(*key_binding, DEFAULT_KEYBINDINGS.quit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.quit.desc); 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); assert_eq!(servarr_context_clues_iter.next(), None);
} }
@@ -204,4 +235,42 @@ mod test {
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
assert_eq!(system_context_clues_iter.next(), None); 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());
}
} }
+6
View File
@@ -23,6 +23,7 @@ generate_keybindings! {
search, search,
auto_search, auto_search,
settings, settings,
help,
filter, filter,
sort, sort,
edit, edit,
@@ -121,6 +122,11 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
alt: None, alt: None,
desc: "settings", desc: "settings",
}, },
help: KeyBinding {
key: Key::Char('?'),
alt: None,
desc: "show/hide keybindings",
},
filter: KeyBinding { filter: KeyBinding {
key: Key::Char('f'), key: Key::Char('f'),
alt: None, alt: None,
+1
View File
@@ -22,6 +22,7 @@ mod test {
#[case(DEFAULT_KEYBINDINGS.auto_search, Key::Char('S'), None, "auto search")] #[case(DEFAULT_KEYBINDINGS.auto_search, Key::Char('S'), None, "auto search")]
#[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), None, "search")] #[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), None, "search")]
#[case(DEFAULT_KEYBINDINGS.settings, Key::Char('S'), None, "settings")] #[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.filter, Key::Char('f'), None, "filter")]
#[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), None, "sort")] #[case(DEFAULT_KEYBINDINGS.sort, Key::Char('o'), None, "sort")]
#[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), None, "edit")] #[case(DEFAULT_KEYBINDINGS.edit, Key::Char('e'), None, "edit")]
+4 -15
View File
@@ -10,10 +10,11 @@ use tokio::sync::mpsc::Sender;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use veil::Redact; use veil::Redact;
use crate::app::context_clues::{build_context_clue_string, SERVARR_CONTEXT_CLUES};
use crate::cli::Command; use crate::cli::Command;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData}; 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::models::{HorizontallyScrollableText, Route, TabRoute, TabState};
use crate::network::NetworkEvent; use crate::network::NetworkEvent;
@@ -32,6 +33,7 @@ pub struct App<'a> {
pub cancellation_token: CancellationToken, pub cancellation_token: CancellationToken,
pub is_first_render: bool, pub is_first_render: bool,
pub server_tabs: TabState, pub server_tabs: TabState,
pub keymapping_table: Option<StatefulTable<KeybindingItem>>,
pub error: HorizontallyScrollableText, pub error: HorizontallyScrollableText,
pub tick_until_poll: u64, pub tick_until_poll: u64,
pub ticks_until_scroll: u64, pub ticks_until_scroll: u64,
@@ -51,10 +53,6 @@ impl App<'_> {
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
) -> Self { ) -> Self {
let mut server_tabs = Vec::new(); let mut server_tabs = Vec::new();
let help = format!(
"<↑↓> scroll | <C-u/d> page up/down | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
);
if let Some(radarr_configs) = config.radarr { if let Some(radarr_configs) = config.radarr {
let mut idx = 0; let mut idx = 0;
@@ -69,7 +67,6 @@ impl App<'_> {
server_tabs.push(TabRoute { server_tabs.push(TabRoute {
title: name, title: name,
route: ActiveRadarrBlock::Movies.into(), route: ActiveRadarrBlock::Movies.into(),
help: help.clone(),
contextual_help: None, contextual_help: None,
config: Some(radarr_config), config: Some(radarr_config),
}); });
@@ -90,7 +87,6 @@ impl App<'_> {
server_tabs.push(TabRoute { server_tabs.push(TabRoute {
title: name, title: name,
route: ActiveSonarrBlock::Series.into(), route: ActiveSonarrBlock::Series.into(),
help: help.clone(),
contextual_help: None, contextual_help: None,
config: Some(sonarr_config), config: Some(sonarr_config),
}); });
@@ -215,6 +211,7 @@ impl Default for App<'_> {
navigation_stack: Vec::new(), navigation_stack: Vec::new(),
network_tx: None, network_tx: None,
cancellation_token: CancellationToken::new(), cancellation_token: CancellationToken::new(),
keymapping_table: None,
error: HorizontallyScrollableText::default(), error: HorizontallyScrollableText::default(),
is_first_render: true, is_first_render: true,
server_tabs: TabState::new(Vec::new()), server_tabs: TabState::new(Vec::new()),
@@ -239,20 +236,12 @@ impl App<'_> {
TabRoute { TabRoute {
title: "Radarr".to_owned(), title: "Radarr".to_owned(),
route: ActiveRadarrBlock::Movies.into(), route: ActiveRadarrBlock::Movies.into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None, contextual_help: None,
config: Some(ServarrConfig::default()), config: Some(ServarrConfig::default()),
}, },
TabRoute { TabRoute {
title: "Sonarr".to_owned(), title: "Sonarr".to_owned(),
route: ActiveSonarrBlock::Series.into(), route: ActiveSonarrBlock::Series.into(),
help: format!(
"<↑↓> scroll | ←→ change tab | {} ",
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
),
contextual_help: None, contextual_help: None,
config: Some(ServarrConfig::default()), config: Some(ServarrConfig::default()),
}, },
+62 -5
View File
@@ -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::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)] #[cfg(test)]
#[path = "radarr_context_clues_tests.rs"] #[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), (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,
DEFAULT_KEYBINDINGS.refresh.desc, 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,
DEFAULT_KEYBINDINGS.auto_search.desc, DEFAULT_KEYBINDINGS.auto_search.desc,
), ),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (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] = [ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "details"), (DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "edit search"), (DEFAULT_KEYBINDINGS.esc, "edit search"),
@@ -86,3 +92,54 @@ pub static COLLECTION_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.edit, "edit collection"), (DEFAULT_KEYBINDINGS.edit, "edit collection"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (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(),
}
}
}
+270 -23
View File
@@ -1,14 +1,21 @@
#[cfg(test)] #[cfg(test)]
mod tests { 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::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::radarr::radarr_context_clues::{ use crate::app::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, RadarrContextClueProvider, ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MOVIE_DETAILS_CONTEXT_CLUES, SYSTEM_TASKS_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] #[test]
fn test_library_context_clues() { fn test_library_context_clues() {
@@ -179,26 +186,15 @@ mod tests {
let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap(); 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_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details"); assert_str_eq!(*description, "details");
assert_eq!(
manual_movie_search_contextual_context_clues_iter.next(), let (key_binding, description) = manual_movie_search_context_clues_iter.next().unwrap();
None
); 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] #[test]
@@ -254,4 +250,255 @@ mod tests {
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(collection_details_context_clues_iter.next(), None); 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<ActiveRadarrBlock>,
) {
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());
}
} }
+95 -13
View File
@@ -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)] #[cfg(test)]
#[path = "sonarr_context_clues_tests.rs"] #[path = "sonarr_context_clues_tests.rs"]
@@ -79,12 +87,7 @@ pub static SERIES_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"),
]; ];
pub static SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES: [ContextClue; 2] = [ pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 7] = [
(DEFAULT_KEYBINDINGS.submit, "episode details"),
(DEFAULT_KEYBINDINGS.delete, "delete episode"),
];
pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
( (
DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc, DEFAULT_KEYBINDINGS.refresh.desc,
@@ -99,9 +102,11 @@ pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [
DEFAULT_KEYBINDINGS.auto_search.desc, DEFAULT_KEYBINDINGS.auto_search.desc,
), ),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.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,
DEFAULT_KEYBINDINGS.refresh.desc, DEFAULT_KEYBINDINGS.refresh.desc,
@@ -113,10 +118,11 @@ pub static SEASON_HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [
DEFAULT_KEYBINDINGS.auto_search, DEFAULT_KEYBINDINGS.auto_search,
DEFAULT_KEYBINDINGS.auto_search.desc, DEFAULT_KEYBINDINGS.auto_search.desc,
), ),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), (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,
DEFAULT_KEYBINDINGS.refresh.desc, DEFAULT_KEYBINDINGS.refresh.desc,
@@ -126,10 +132,11 @@ pub static MANUAL_SEASON_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [
DEFAULT_KEYBINDINGS.auto_search.desc, DEFAULT_KEYBINDINGS.auto_search.desc,
), ),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (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,
DEFAULT_KEYBINDINGS.refresh.desc, DEFAULT_KEYBINDINGS.refresh.desc,
@@ -139,12 +146,10 @@ pub static MANUAL_EPISODE_SEARCH_CONTEXT_CLUES: [ContextClue; 4] = [
DEFAULT_KEYBINDINGS.auto_search.desc, DEFAULT_KEYBINDINGS.auto_search.desc,
), ),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (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] = [ pub static EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
( (
DEFAULT_KEYBINDINGS.refresh, DEFAULT_KEYBINDINGS.refresh,
@@ -157,7 +162,84 @@ pub static EPISODE_DETAILS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (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] = [ pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
(DEFAULT_KEYBINDINGS.submit, "start task"), (DEFAULT_KEYBINDINGS.submit, "start task"),
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc), (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(),
}
}
}
+318 -26
View File
@@ -1,17 +1,29 @@
#[cfg(test)] #[cfg(test)]
mod tests { 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::{ use crate::app::{
key_binding::DEFAULT_KEYBINDINGS, key_binding::DEFAULT_KEYBINDINGS,
sonarr::sonarr_context_clues::{ sonarr::sonarr_context_clues::{
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, DETAILS_CONTEXTUAL_CONTEXT_CLUES, ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES,
EPISODE_DETAILS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES,
MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES,
SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES,
SERIES_DETAILS_CONTEXT_CLUES, SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_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] #[test]
fn test_add_series_search_results_context_clues() { fn test_add_series_search_results_context_clues() {
@@ -252,23 +264,18 @@ mod tests {
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(season_details_context_clues_iter.next(), None);
}
#[test] let (key_binding, description) = season_details_context_clues_iter.next().unwrap();
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();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "episode details"); 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_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, "delete episode"); 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] #[test]
@@ -301,6 +308,11 @@ mod tests {
let (key_binding, description) = season_history_context_clues_iter.next().unwrap(); 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_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, "cancel filter/close"); assert_str_eq!(*description, "cancel filter/close");
assert_eq!(season_history_context_clues_iter.next(), None); 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(); 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_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(manual_season_search_context_clues_iter.next(), None); 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(); 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_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(manual_episode_search_context_clues_iter.next(), None); 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] #[test]
fn test_episode_details_context_clues() { fn test_episode_details_context_clues() {
let mut episode_details_context_clues_iter = EPISODE_DETAILS_CONTEXT_CLUES.iter(); 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); 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] #[test]
fn test_system_tasks_context_clues() { fn test_system_tasks_context_clues() {
let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter(); 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_str_eq!(*description, DEFAULT_KEYBINDINGS.esc.desc);
assert_eq!(system_tasks_context_clues_iter.next(), None); 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());
}
} }
+17 -17
View File
@@ -31,23 +31,23 @@ pub enum Key {
impl Display for Key { impl Display for Key {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match *self {
Key::Char(c) => write!(f, "<{c}>"), Key::Char(c) => write!(f, "{c}"),
Key::Ctrl(c) => write!(f, "<C-{c}>"), Key::Ctrl(c) => write!(f, "ctrl-{c}"),
Key::Up => write!(f, "<>"), Key::Up => write!(f, ""),
Key::Down => write!(f, "<>"), Key::Down => write!(f, ""),
Key::Left => write!(f, "<>"), Key::Left => write!(f, ""),
Key::Right => write!(f, "<>"), Key::Right => write!(f, ""),
Key::PgDown => write!(f, "<C-d>"), Key::PgDown => write!(f, "pgDown"),
Key::PgUp => write!(f, "<C-u>"), Key::PgUp => write!(f, "pgUp"),
Key::Enter => write!(f, "<enter>"), Key::Enter => write!(f, "enter"),
Key::Esc => write!(f, "<esc>"), Key::Esc => write!(f, "esc"),
Key::Backspace => write!(f, "<backspace>"), Key::Backspace => write!(f, "backspace"),
Key::Home => write!(f, "<home>"), Key::Home => write!(f, "home"),
Key::End => write!(f, "<end>"), Key::End => write!(f, "end"),
Key::Tab => write!(f, "<tab>"), Key::Tab => write!(f, "tab"),
Key::BackTab => write!(f, "<shift-tab>"), Key::BackTab => write!(f, "shift-tab"),
Key::Delete => write!(f, "<del>"), Key::Delete => write!(f, "del"),
_ => write!(f, "<{self:?}>"), _ => write!(f, "{self:?}"),
} }
} }
} }
+4 -4
View File
@@ -11,8 +11,8 @@ mod tests {
#[case(Key::Down, "")] #[case(Key::Down, "")]
#[case(Key::Left, "")] #[case(Key::Left, "")]
#[case(Key::Right, "")] #[case(Key::Right, "")]
#[case(Key::PgDown, "C-d")] #[case(Key::PgDown, "pgDown")]
#[case(Key::PgUp, "C-u")] #[case(Key::PgUp, "pgUp")]
#[case(Key::Enter, "enter")] #[case(Key::Enter, "enter")]
#[case(Key::Esc, "esc")] #[case(Key::Esc, "esc")]
#[case(Key::Backspace, "backspace")] #[case(Key::Backspace, "backspace")]
@@ -22,9 +22,9 @@ mod tests {
#[case(Key::BackTab, "shift-tab")] #[case(Key::BackTab, "shift-tab")]
#[case(Key::Delete, "del")] #[case(Key::Delete, "del")]
#[case(Key::Char('q'), "q")] #[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) { 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] #[test]
+177 -3
View File
@@ -6,13 +6,20 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use tokio_util::sync::CancellationToken; 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::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::handle_events;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle}; 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::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::HorizontallyScrollableText;
use crate::models::Route; use crate::models::Route;
@@ -82,6 +89,82 @@ mod tests {
assert!(app.cancellation_token.is_cancelled()); 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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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] #[rstest]
fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) { fn test_handle_prompt_toggle_left_right_radarr(#[values(Key::Left, Key::Right)] key: Key) {
let mut app = App::test_default(); let mut app = App::test_default();
@@ -113,4 +196,95 @@ mod tests {
assert!(!app.data.sonarr_data.prompt_confirm); 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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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(),
}
}
} }
+80
View File
@@ -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<ActiveKeybindingBlock>,
) -> 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) {}
}
+69
View File
@@ -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());
}
}
+60
View File
@@ -1,11 +1,20 @@
use radarr_handlers::RadarrHandler; use radarr_handlers::RadarrHandler;
use sonarr_handlers::SonarrHandler; 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::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::keybinding_handler::KeybindingHandler;
use crate::matches_key; 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}; use crate::models::{HorizontallyScrollableText, Route};
mod keybinding_handler;
mod radarr_handlers; mod radarr_handlers;
mod sonarr_handlers; mod sonarr_handlers;
@@ -97,8 +106,17 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
app.server_tabs.previous(); app.server_tabs.previous();
app.pop_and_push_navigation_stack(app.server_tabs.get_active_route()); app.pop_and_push_navigation_stack(app.server_tabs.get_active_route());
app.cancellation_token.cancel(); 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 { } else {
match app.get_current_route() { 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) => { Route::Radarr(active_radarr_block, context) => {
RadarrHandler::new(key, app, active_radarr_block, context).handle() 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::<Vec<_>>();
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<'_>) { fn handle_clear_errors(app: &mut App<'_>) {
if !app.error.text.is_empty() { if !app.error.text.is_empty() {
app.error = HorizontallyScrollableText::default(); app.error = HorizontallyScrollableText::default();
+7 -9
View File
@@ -1,6 +1,7 @@
use std::fmt::{Debug, Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use crate::app::context_clues::ContextClue;
use crate::app::ServarrConfig; use crate::app::ServarrConfig;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use radarr_models::RadarrSerdeable; use radarr_models::RadarrSerdeable;
@@ -9,6 +10,7 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Number; use serde_json::Number;
use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use sonarr_models::SonarrSerdeable; use sonarr_models::SonarrSerdeable;
pub mod radarr_models; pub mod radarr_models;
pub mod servarr_data; pub mod servarr_data;
pub mod servarr_models; pub mod servarr_models;
@@ -33,6 +35,7 @@ pub enum Route {
Bazarr, Bazarr,
Prowlarr, Prowlarr,
Tautulli, Tautulli,
Keybindings,
} }
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
@@ -271,8 +274,7 @@ impl HorizontallyScrollableText {
pub struct TabRoute { pub struct TabRoute {
pub title: String, pub title: String,
pub route: Route, pub route: Route,
pub help: String, pub contextual_help: Option<&'static [ContextClue]>,
pub contextual_help: Option<String>,
pub config: Option<ServarrConfig>, pub config: Option<ServarrConfig>,
} }
@@ -286,7 +288,7 @@ impl TabState {
TabState { tabs, index: 0 } 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)] #[allow(dead_code)]
pub fn set_index(&mut self, index: usize) -> &TabRoute { pub fn set_index(&mut self, index: usize) -> &TabRoute {
self.index = index; self.index = index;
@@ -337,12 +339,8 @@ impl TabState {
false false
} }
pub fn get_active_tab_help(&self) -> &str { pub fn get_active_route_contextual_help(&self) -> Option<&'static [ContextClue]> {
&self.tabs[self.index].help self.tabs[self.index].contextual_help
}
pub fn get_active_tab_contextual_help(&self) -> Option<String> {
self.tabs[self.index].contextual_help.clone()
} }
pub fn next(&mut self) { pub fn next(&mut self) {
+13 -20
View File
@@ -3,6 +3,8 @@ mod tests {
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use crate::app::context_clues::ContextClue;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::ServarrConfig; use crate::app::ServarrConfig;
use crate::models::from_f64; use crate::models::from_f64;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
@@ -18,6 +20,10 @@ mod tests {
use serde_json::to_string; use serde_json::to_string;
const BLOCKS: &[&[i32]] = &[&[11, 12], &[21, 22], &[31, 32]]; 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] #[test]
fn test_scrollable_text_with_string() { fn test_scrollable_text_with_string() {
@@ -617,26 +623,15 @@ mod tests {
} }
#[test] #[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 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_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); assert!(tab_help.is_some());
} assert_eq!(tab_help.unwrap(), second_tab_help.unwrap());
#[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);
} }
#[test] #[test]
@@ -823,15 +818,13 @@ mod tests {
TabRoute { TabRoute {
title: "Test 1".to_owned(), title: "Test 1".to_owned(),
route: ActiveRadarrBlock::Movies.into(), route: ActiveRadarrBlock::Movies.into(),
help: "Help for Test 1".to_owned(), contextual_help: Some(&HELP_KEYBINDINGS),
contextual_help: Some("Contextual Help for Test 1".to_owned()),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Test 2".to_owned(), title: "Test 2".to_owned(),
route: ActiveRadarrBlock::Collections.into(), route: ActiveRadarrBlock::Collections.into(),
help: "Help for Test 2".to_owned(), contextual_help: Some(&ESC_KEYBINDINGS),
contextual_help: Some("Contextual Help for Test 2".to_owned()),
config: None, config: None,
}, },
] ]
+18
View File
@@ -1,3 +1,21 @@
use crate::models::Route;
pub mod modals; pub mod modals;
pub mod radarr; pub mod radarr;
pub mod sonarr; 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<ActiveKeybindingBlock> for Route {
fn from(_active_keybinding_block: ActiveKeybindingBlock) -> Route {
Route::Keybindings
}
}
+17 -32
View File
@@ -1,10 +1,10 @@
use crate::app::context_clues::{ use crate::app::context_clues::{
build_context_clue_string, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
}; };
use crate::app::radarr::radarr_context_clues::{ use crate::app::radarr::radarr_context_clues::{
COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES,
}; };
use crate::models::radarr_models::{ use crate::models::radarr_models::{
AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DownloadRecord, AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DownloadRecord,
@@ -123,50 +123,43 @@ impl<'a> Default for RadarrData<'a> {
TabRoute { TabRoute {
title: "Library".to_string(), title: "Library".to_string(),
route: ActiveRadarrBlock::Movies.into(), route: ActiveRadarrBlock::Movies.into(),
help: String::new(), contextual_help: Some(&LIBRARY_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Collections".to_string(), title: "Collections".to_string(),
route: ActiveRadarrBlock::Collections.into(), route: ActiveRadarrBlock::Collections.into(),
help: String::new(), contextual_help: Some(&COLLECTIONS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Downloads".to_string(), title: "Downloads".to_string(),
route: ActiveRadarrBlock::Downloads.into(), route: ActiveRadarrBlock::Downloads.into(),
help: String::new(), contextual_help: Some(&DOWNLOADS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Blocklist".to_string(), title: "Blocklist".to_string(),
route: ActiveRadarrBlock::Blocklist.into(), route: ActiveRadarrBlock::Blocklist.into(),
help: String::new(), contextual_help: Some(&BLOCKLIST_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Root Folders".to_string(), title: "Root Folders".to_string(),
route: ActiveRadarrBlock::RootFolders.into(), route: ActiveRadarrBlock::RootFolders.into(),
help: String::new(), contextual_help: Some(&ROOT_FOLDERS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Indexers".to_string(), title: "Indexers".to_string(),
route: ActiveRadarrBlock::Indexers.into(), route: ActiveRadarrBlock::Indexers.into(),
help: String::new(), contextual_help: Some(&INDEXERS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "System".to_string(), title: "System".to_string(),
route: ActiveRadarrBlock::System.into(), route: ActiveRadarrBlock::System.into(),
help: String::new(), contextual_help: Some(&SYSTEM_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)),
config: None, config: None,
}, },
]), ]),
@@ -174,45 +167,37 @@ impl<'a> Default for RadarrData<'a> {
TabRoute { TabRoute {
title: "Details".to_string(), title: "Details".to_string(),
route: ActiveRadarrBlock::MovieDetails.into(), route: ActiveRadarrBlock::MovieDetails.into(),
help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), contextual_help: Some(&MOVIE_DETAILS_CONTEXT_CLUES),
contextual_help: None,
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "History".to_string(), title: "History".to_string(),
route: ActiveRadarrBlock::MovieHistory.into(), route: ActiveRadarrBlock::MovieHistory.into(),
help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), contextual_help: Some(&MOVIE_DETAILS_CONTEXT_CLUES),
contextual_help: None,
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "File".to_string(), title: "File".to_string(),
route: ActiveRadarrBlock::FileInfo.into(), route: ActiveRadarrBlock::FileInfo.into(),
help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), contextual_help: Some(&MOVIE_DETAILS_CONTEXT_CLUES),
contextual_help: None,
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Cast".to_string(), title: "Cast".to_string(),
route: ActiveRadarrBlock::Cast.into(), route: ActiveRadarrBlock::Cast.into(),
help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), contextual_help: Some(&MOVIE_DETAILS_CONTEXT_CLUES),
contextual_help: None,
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Crew".to_string(), title: "Crew".to_string(),
route: ActiveRadarrBlock::Crew.into(), route: ActiveRadarrBlock::Crew.into(),
help: build_context_clue_string(&MOVIE_DETAILS_CONTEXT_CLUES), contextual_help: Some(&MOVIE_DETAILS_CONTEXT_CLUES),
contextual_help: None,
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Manual Search".to_string(), title: "Manual Search".to_string(),
route: ActiveRadarrBlock::ManualSearch.into(), route: ActiveRadarrBlock::ManualSearch.into(),
help: build_context_clue_string(&MANUAL_MOVIE_SEARCH_CONTEXT_CLUES), contextual_help: Some(&MANUAL_MOVIE_SEARCH_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(
&MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
)),
config: None, config: None,
}, },
]), ]),
@@ -5,12 +5,11 @@ mod tests {
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use crate::app::context_clues::{ use crate::app::context_clues::{
build_context_clue_string, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
}; };
use crate::app::radarr::radarr_context_clues::{ use crate::app::radarr::radarr_context_clues::{
COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
MOVIE_DETAILS_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES,
}; };
@@ -107,10 +106,10 @@ mod tests {
radarr_data.main_tabs.tabs[0].route, radarr_data.main_tabs.tabs[0].route,
ActiveRadarrBlock::Movies.into() 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!( assert_eq!(
radarr_data.main_tabs.tabs[0].contextual_help, radarr_data.main_tabs.tabs[0].contextual_help.unwrap(),
Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES)) &LIBRARY_CONTEXT_CLUES
); );
assert_eq!(radarr_data.main_tabs.tabs[0].config, None); assert_eq!(radarr_data.main_tabs.tabs[0].config, None);
@@ -119,10 +118,10 @@ mod tests {
radarr_data.main_tabs.tabs[1].route, radarr_data.main_tabs.tabs[1].route,
ActiveRadarrBlock::Collections.into() 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!( assert_eq!(
radarr_data.main_tabs.tabs[1].contextual_help, radarr_data.main_tabs.tabs[1].contextual_help.unwrap(),
Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)) &COLLECTIONS_CONTEXT_CLUES
); );
assert_eq!(radarr_data.main_tabs.tabs[1].config, None); assert_eq!(radarr_data.main_tabs.tabs[1].config, None);
@@ -131,10 +130,10 @@ mod tests {
radarr_data.main_tabs.tabs[2].route, radarr_data.main_tabs.tabs[2].route,
ActiveRadarrBlock::Downloads.into() 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!( assert_eq!(
radarr_data.main_tabs.tabs[2].contextual_help, radarr_data.main_tabs.tabs[2].contextual_help.unwrap(),
Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)) &DOWNLOADS_CONTEXT_CLUES
); );
assert_eq!(radarr_data.main_tabs.tabs[2].config, None); assert_eq!(radarr_data.main_tabs.tabs[2].config, None);
@@ -143,10 +142,10 @@ mod tests {
radarr_data.main_tabs.tabs[3].route, radarr_data.main_tabs.tabs[3].route,
ActiveRadarrBlock::Blocklist.into() 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!( assert_eq!(
radarr_data.main_tabs.tabs[3].contextual_help, radarr_data.main_tabs.tabs[3].contextual_help.unwrap(),
Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)) &BLOCKLIST_CONTEXT_CLUES
); );
assert_eq!(radarr_data.main_tabs.tabs[3].config, None); assert_eq!(radarr_data.main_tabs.tabs[3].config, None);
@@ -155,10 +154,10 @@ mod tests {
radarr_data.main_tabs.tabs[4].route, radarr_data.main_tabs.tabs[4].route,
ActiveRadarrBlock::RootFolders.into() 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!( assert_eq!(
radarr_data.main_tabs.tabs[4].contextual_help, radarr_data.main_tabs.tabs[4].contextual_help.unwrap(),
Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)) &ROOT_FOLDERS_CONTEXT_CLUES
); );
assert_eq!(radarr_data.main_tabs.tabs[4].config, None); assert_eq!(radarr_data.main_tabs.tabs[4].config, None);
@@ -167,10 +166,10 @@ mod tests {
radarr_data.main_tabs.tabs[5].route, radarr_data.main_tabs.tabs[5].route,
ActiveRadarrBlock::Indexers.into() 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!( assert_eq!(
radarr_data.main_tabs.tabs[5].contextual_help, radarr_data.main_tabs.tabs[5].contextual_help.unwrap(),
Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)) &INDEXERS_CONTEXT_CLUES
); );
assert_eq!(radarr_data.main_tabs.tabs[5].config, None); assert_eq!(radarr_data.main_tabs.tabs[5].config, None);
@@ -179,10 +178,10 @@ mod tests {
radarr_data.main_tabs.tabs[6].route, radarr_data.main_tabs.tabs[6].route,
ActiveRadarrBlock::System.into() 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!( assert_eq!(
radarr_data.main_tabs.tabs[6].contextual_help, radarr_data.main_tabs.tabs[6].contextual_help.unwrap(),
Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)) &SYSTEM_CONTEXT_CLUES
); );
assert_eq!(radarr_data.main_tabs.tabs[6].config, None); assert_eq!(radarr_data.main_tabs.tabs[6].config, None);
@@ -193,13 +192,13 @@ mod tests {
radarr_data.movie_info_tabs.tabs[0].route, radarr_data.movie_info_tabs.tabs[0].route,
ActiveRadarrBlock::MovieDetails.into() 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] assert!(radarr_data.movie_info_tabs.tabs[0]
.contextual_help .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_eq!(radarr_data.movie_info_tabs.tabs[0].config, None);
assert_str_eq!(radarr_data.movie_info_tabs.tabs[1].title, "History"); 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, radarr_data.movie_info_tabs.tabs[1].route,
ActiveRadarrBlock::MovieHistory.into() 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] assert!(radarr_data.movie_info_tabs.tabs[1]
.contextual_help .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_eq!(radarr_data.movie_info_tabs.tabs[1].config, None);
assert_str_eq!(radarr_data.movie_info_tabs.tabs[2].title, "File"); 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, radarr_data.movie_info_tabs.tabs[2].route,
ActiveRadarrBlock::FileInfo.into() 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] assert!(radarr_data.movie_info_tabs.tabs[2]
.contextual_help .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_eq!(radarr_data.movie_info_tabs.tabs[2].config, None);
assert_str_eq!(radarr_data.movie_info_tabs.tabs[3].title, "Cast"); 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, radarr_data.movie_info_tabs.tabs[3].route,
ActiveRadarrBlock::Cast.into() 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] assert!(radarr_data.movie_info_tabs.tabs[3]
.contextual_help .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_eq!(radarr_data.movie_info_tabs.tabs[3].config, None);
assert_str_eq!(radarr_data.movie_info_tabs.tabs[4].title, "Crew"); 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, radarr_data.movie_info_tabs.tabs[4].route,
ActiveRadarrBlock::Crew.into() 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] assert!(radarr_data.movie_info_tabs.tabs[4]
.contextual_help .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_eq!(radarr_data.movie_info_tabs.tabs[4].config, None);
assert_str_eq!(radarr_data.movie_info_tabs.tabs[5].title, "Manual Search"); 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, radarr_data.movie_info_tabs.tabs[5].route,
ActiveRadarrBlock::ManualSearch.into() ActiveRadarrBlock::ManualSearch.into()
); );
assert_str_eq!( assert!(radarr_data.movie_info_tabs.tabs[5]
radarr_data.movie_info_tabs.tabs[5].help, .contextual_help
build_context_clue_string(&MANUAL_MOVIE_SEARCH_CONTEXT_CLUES) .is_some());
);
assert_eq!( assert_eq!(
radarr_data.movie_info_tabs.tabs[5].contextual_help, radarr_data.movie_info_tabs.tabs[5].contextual_help.unwrap(),
Some(build_context_clue_string( &MANUAL_MOVIE_SEARCH_CONTEXT_CLUES
&MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES
))
); );
assert_eq!(radarr_data.movie_info_tabs.tabs[5].config, None); assert_eq!(radarr_data.movie_info_tabs.tabs[5].config, None);
} }
@@ -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);
}
}
+11 -24
View File
@@ -1,16 +1,12 @@
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use super::sonarr_data::{ActiveSonarrBlock, SonarrData}; 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::models::sonarr_models::EpisodeFile;
use crate::{ use crate::{
app::{ app::sonarr::sonarr_context_clues::{
context_clues::build_context_clue_string, EPISODE_DETAILS_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES,
sonarr::sonarr_context_clues::{ MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_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,
},
}, },
models::{ models::{
servarr_data::modals::EditIndexerModal, servarr_data::modals::EditIndexerModal,
@@ -282,29 +278,25 @@ impl Default for EpisodeDetailsModal {
TabRoute { TabRoute {
title: "Details".to_string(), title: "Details".to_string(),
route: ActiveSonarrBlock::EpisodeDetails.into(), route: ActiveSonarrBlock::EpisodeDetails.into(),
help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), contextual_help: Some(&EPISODE_DETAILS_CONTEXT_CLUES),
contextual_help: None,
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "History".to_string(), title: "History".to_string(),
route: ActiveSonarrBlock::EpisodeHistory.into(), route: ActiveSonarrBlock::EpisodeHistory.into(),
help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), contextual_help: Some(&SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "File".to_string(), title: "File".to_string(),
route: ActiveSonarrBlock::EpisodeFile.into(), route: ActiveSonarrBlock::EpisodeFile.into(),
help: build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES), contextual_help: Some(&EPISODE_DETAILS_CONTEXT_CLUES),
contextual_help: None,
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Manual Search".to_string(), title: "Manual Search".to_string(),
route: ActiveSonarrBlock::ManualEpisodeSearch.into(), route: ActiveSonarrBlock::ManualEpisodeSearch.into(),
help: build_context_clue_string(&MANUAL_EPISODE_SEARCH_CONTEXT_CLUES), contextual_help: Some(&MANUAL_EPISODE_SEARCH_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)),
config: None, config: None,
}, },
]), ]),
@@ -333,24 +325,19 @@ impl Default for SeasonDetailsModal {
TabRoute { TabRoute {
title: "Episodes".to_string(), title: "Episodes".to_string(),
route: ActiveSonarrBlock::SeasonDetails.into(), route: ActiveSonarrBlock::SeasonDetails.into(),
help: build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES), contextual_help: Some(&SEASON_DETAILS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(
&SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES,
)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "History".to_string(), title: "History".to_string(),
route: ActiveSonarrBlock::SeasonHistory.into(), route: ActiveSonarrBlock::SeasonHistory.into(),
help: build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES), contextual_help: Some(&SEASON_HISTORY_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Manual Search".to_string(), title: "Manual Search".to_string(),
route: ActiveSonarrBlock::ManualSeasonSearch.into(), route: ActiveSonarrBlock::ManualSeasonSearch.into(),
help: build_context_clue_string(&MANUAL_SEASON_SEARCH_CONTEXT_CLUES), contextual_help: Some(&MANUAL_SEASON_SEARCH_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)),
config: None, config: None,
}, },
]), ]),
+52 -47
View File
@@ -5,12 +5,10 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::app::context_clues::build_context_clue_string;
use crate::app::sonarr::sonarr_context_clues::{ use crate::app::sonarr::sonarr_context_clues::{
DETAILS_CONTEXTUAL_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES,
MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, SEASON_HISTORY_CONTEXT_CLUES,
SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES,
SEASON_HISTORY_CONTEXT_CLUES,
}; };
use crate::models::servarr_data::sonarr::modals::{ use crate::models::servarr_data::sonarr::modals::{
EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal, EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal,
@@ -257,13 +255,15 @@ mod tests {
episode_details_modal.episode_details_tabs.tabs[0].route, episode_details_modal.episode_details_tabs.tabs[0].route,
ActiveSonarrBlock::EpisodeDetails.into() 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] assert!(episode_details_modal.episode_details_tabs.tabs[0]
.contextual_help .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!( assert_eq!(
episode_details_modal.episode_details_tabs.tabs[0].config, episode_details_modal.episode_details_tabs.tabs[0].config,
None None
@@ -277,13 +277,14 @@ mod tests {
episode_details_modal.episode_details_tabs.tabs[1].route, episode_details_modal.episode_details_tabs.tabs[1].route,
ActiveSonarrBlock::EpisodeHistory.into() ActiveSonarrBlock::EpisodeHistory.into()
); );
assert_str_eq!( assert!(episode_details_modal.episode_details_tabs.tabs[1]
episode_details_modal.episode_details_tabs.tabs[1].help, .contextual_help
build_context_clue_string(&EPISODE_DETAILS_CONTEXT_CLUES) .is_some());
);
assert_eq!( assert_eq!(
episode_details_modal.episode_details_tabs.tabs[1].contextual_help, episode_details_modal.episode_details_tabs.tabs[1]
Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)) .contextual_help
.unwrap(),
&SELECTABLE_EPISODE_DETAILS_CONTEXT_CLUES
); );
assert_eq!( assert_eq!(
episode_details_modal.episode_details_tabs.tabs[1].config, episode_details_modal.episode_details_tabs.tabs[1].config,
@@ -298,13 +299,15 @@ mod tests {
episode_details_modal.episode_details_tabs.tabs[2].route, episode_details_modal.episode_details_tabs.tabs[2].route,
ActiveSonarrBlock::EpisodeFile.into() 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] assert!(episode_details_modal.episode_details_tabs.tabs[2]
.contextual_help .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!( assert_eq!(
episode_details_modal.episode_details_tabs.tabs[2].config, episode_details_modal.episode_details_tabs.tabs[2].config,
None None
@@ -318,13 +321,14 @@ mod tests {
episode_details_modal.episode_details_tabs.tabs[3].route, episode_details_modal.episode_details_tabs.tabs[3].route,
ActiveSonarrBlock::ManualEpisodeSearch.into() ActiveSonarrBlock::ManualEpisodeSearch.into()
); );
assert_str_eq!( assert!(episode_details_modal.episode_details_tabs.tabs[3]
episode_details_modal.episode_details_tabs.tabs[3].help, .contextual_help
build_context_clue_string(&MANUAL_EPISODE_SEARCH_CONTEXT_CLUES) .is_some());
);
assert_eq!( assert_eq!(
episode_details_modal.episode_details_tabs.tabs[3].contextual_help, episode_details_modal.episode_details_tabs.tabs[3]
Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)) .contextual_help
.unwrap(),
&MANUAL_EPISODE_SEARCH_CONTEXT_CLUES
); );
assert_eq!( assert_eq!(
episode_details_modal.episode_details_tabs.tabs[3].config, episode_details_modal.episode_details_tabs.tabs[3].config,
@@ -352,15 +356,14 @@ mod tests {
season_details_modal.season_details_tabs.tabs[0].route, season_details_modal.season_details_tabs.tabs[0].route,
ActiveSonarrBlock::SeasonDetails.into() ActiveSonarrBlock::SeasonDetails.into()
); );
assert_str_eq!( assert!(season_details_modal.season_details_tabs.tabs[0]
season_details_modal.season_details_tabs.tabs[0].help, .contextual_help
build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES) .is_some());
);
assert_eq!( assert_eq!(
season_details_modal.season_details_tabs.tabs[0].contextual_help, season_details_modal.season_details_tabs.tabs[0]
Some(build_context_clue_string( .contextual_help
&SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES .unwrap(),
)) &SEASON_DETAILS_CONTEXT_CLUES
); );
assert_eq!( assert_eq!(
season_details_modal.season_details_tabs.tabs[0].config, season_details_modal.season_details_tabs.tabs[0].config,
@@ -375,13 +378,14 @@ mod tests {
season_details_modal.season_details_tabs.tabs[1].route, season_details_modal.season_details_tabs.tabs[1].route,
ActiveSonarrBlock::SeasonHistory.into() ActiveSonarrBlock::SeasonHistory.into()
); );
assert_str_eq!( assert!(season_details_modal.season_details_tabs.tabs[1]
season_details_modal.season_details_tabs.tabs[1].help, .contextual_help
build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES) .is_some());
);
assert_eq!( assert_eq!(
season_details_modal.season_details_tabs.tabs[1].contextual_help, season_details_modal.season_details_tabs.tabs[1]
Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)) .contextual_help
.unwrap(),
&SEASON_HISTORY_CONTEXT_CLUES
); );
assert_eq!( assert_eq!(
season_details_modal.season_details_tabs.tabs[1].config, season_details_modal.season_details_tabs.tabs[1].config,
@@ -396,13 +400,14 @@ mod tests {
season_details_modal.season_details_tabs.tabs[2].route, season_details_modal.season_details_tabs.tabs[2].route,
ActiveSonarrBlock::ManualSeasonSearch.into() ActiveSonarrBlock::ManualSeasonSearch.into()
); );
assert_str_eq!( assert!(season_details_modal.season_details_tabs.tabs[2]
season_details_modal.season_details_tabs.tabs[2].help, .contextual_help
build_context_clue_string(&MANUAL_SEASON_SEARCH_CONTEXT_CLUES) .is_some());
);
assert_eq!( assert_eq!(
season_details_modal.season_details_tabs.tabs[2].contextual_help, season_details_modal.season_details_tabs.tabs[2]
Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)) .contextual_help
.unwrap(),
&MANUAL_SEASON_SEARCH_CONTEXT_CLUES
); );
assert_eq!( assert_eq!(
season_details_modal.season_details_tabs.tabs[2].config, season_details_modal.season_details_tabs.tabs[2].config,
+11 -20
View File
@@ -5,8 +5,8 @@ use strum::EnumIter;
use crate::{ use crate::{
app::{ app::{
context_clues::{ context_clues::{
build_context_clue_string, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
}, },
sonarr::sonarr_context_clues::{ sonarr::sonarr_context_clues::{
HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES,
@@ -130,50 +130,43 @@ impl<'a> Default for SonarrData<'a> {
TabRoute { TabRoute {
title: "Library".to_string(), title: "Library".to_string(),
route: ActiveSonarrBlock::Series.into(), route: ActiveSonarrBlock::Series.into(),
help: String::new(), contextual_help: Some(&SERIES_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&SERIES_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Downloads".to_string(), title: "Downloads".to_string(),
route: ActiveSonarrBlock::Downloads.into(), route: ActiveSonarrBlock::Downloads.into(),
help: String::new(), contextual_help: Some(&DOWNLOADS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Blocklist".to_string(), title: "Blocklist".to_string(),
route: ActiveSonarrBlock::Blocklist.into(), route: ActiveSonarrBlock::Blocklist.into(),
help: String::new(), contextual_help: Some(&BLOCKLIST_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "History".to_string(), title: "History".to_string(),
route: ActiveSonarrBlock::History.into(), route: ActiveSonarrBlock::History.into(),
help: String::new(), contextual_help: Some(&HISTORY_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Root Folders".to_string(), title: "Root Folders".to_string(),
route: ActiveSonarrBlock::RootFolders.into(), route: ActiveSonarrBlock::RootFolders.into(),
help: String::new(), contextual_help: Some(&ROOT_FOLDERS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "Indexers".to_string(), title: "Indexers".to_string(),
route: ActiveSonarrBlock::Indexers.into(), route: ActiveSonarrBlock::Indexers.into(),
help: String::new(), contextual_help: Some(&INDEXERS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "System".to_string(), title: "System".to_string(),
route: ActiveSonarrBlock::System.into(), route: ActiveSonarrBlock::System.into(),
help: String::new(), contextual_help: Some(&SYSTEM_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)),
config: None, config: None,
}, },
]), ]),
@@ -181,15 +174,13 @@ impl<'a> Default for SonarrData<'a> {
TabRoute { TabRoute {
title: "Seasons".to_string(), title: "Seasons".to_string(),
route: ActiveSonarrBlock::SeriesDetails.into(), route: ActiveSonarrBlock::SeriesDetails.into(),
help: String::new(), contextual_help: Some(&SERIES_DETAILS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&SERIES_DETAILS_CONTEXT_CLUES)),
config: None, config: None,
}, },
TabRoute { TabRoute {
title: "History".to_string(), title: "History".to_string(),
route: ActiveSonarrBlock::SeriesHistory.into(), route: ActiveSonarrBlock::SeriesHistory.into(),
help: String::new(), contextual_help: Some(&SERIES_HISTORY_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES)),
config: None, config: None,
}, },
]), ]),
@@ -10,8 +10,8 @@ mod tests {
use crate::{ use crate::{
app::{ app::{
context_clues::{ context_clues::{
build_context_clue_string, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
}, },
sonarr::sonarr_context_clues::{ sonarr::sonarr_context_clues::{
HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_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, sonarr_data.main_tabs.tabs[0].route,
ActiveSonarrBlock::Series.into() 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!( assert_eq!(
sonarr_data.main_tabs.tabs[0].contextual_help, sonarr_data.main_tabs.tabs[0].contextual_help.unwrap(),
Some(build_context_clue_string(&SERIES_CONTEXT_CLUES)) &SERIES_CONTEXT_CLUES
); );
assert_eq!(sonarr_data.main_tabs.tabs[0].config, None); assert_eq!(sonarr_data.main_tabs.tabs[0].config, None);
@@ -135,10 +135,10 @@ mod tests {
sonarr_data.main_tabs.tabs[1].route, sonarr_data.main_tabs.tabs[1].route,
ActiveSonarrBlock::Downloads.into() 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!( assert_eq!(
sonarr_data.main_tabs.tabs[1].contextual_help, sonarr_data.main_tabs.tabs[1].contextual_help.unwrap(),
Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)) &DOWNLOADS_CONTEXT_CLUES
); );
assert_eq!(sonarr_data.main_tabs.tabs[1].config, None); assert_eq!(sonarr_data.main_tabs.tabs[1].config, None);
@@ -147,10 +147,10 @@ mod tests {
sonarr_data.main_tabs.tabs[2].route, sonarr_data.main_tabs.tabs[2].route,
ActiveSonarrBlock::Blocklist.into() 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!( assert_eq!(
sonarr_data.main_tabs.tabs[2].contextual_help, sonarr_data.main_tabs.tabs[2].contextual_help.unwrap(),
Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)) &BLOCKLIST_CONTEXT_CLUES
); );
assert_eq!(sonarr_data.main_tabs.tabs[2].config, None); assert_eq!(sonarr_data.main_tabs.tabs[2].config, None);
@@ -159,10 +159,10 @@ mod tests {
sonarr_data.main_tabs.tabs[3].route, sonarr_data.main_tabs.tabs[3].route,
ActiveSonarrBlock::History.into() 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!( assert_eq!(
sonarr_data.main_tabs.tabs[3].contextual_help, sonarr_data.main_tabs.tabs[3].contextual_help.unwrap(),
Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)) &HISTORY_CONTEXT_CLUES
); );
assert_eq!(sonarr_data.main_tabs.tabs[3].config, None); assert_eq!(sonarr_data.main_tabs.tabs[3].config, None);
@@ -171,10 +171,10 @@ mod tests {
sonarr_data.main_tabs.tabs[4].route, sonarr_data.main_tabs.tabs[4].route,
ActiveSonarrBlock::RootFolders.into() 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!( assert_eq!(
sonarr_data.main_tabs.tabs[4].contextual_help, sonarr_data.main_tabs.tabs[4].contextual_help.unwrap(),
Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)) &ROOT_FOLDERS_CONTEXT_CLUES
); );
assert_eq!(sonarr_data.main_tabs.tabs[4].config, None); assert_eq!(sonarr_data.main_tabs.tabs[4].config, None);
@@ -183,10 +183,10 @@ mod tests {
sonarr_data.main_tabs.tabs[5].route, sonarr_data.main_tabs.tabs[5].route,
ActiveSonarrBlock::Indexers.into() 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!( assert_eq!(
sonarr_data.main_tabs.tabs[5].contextual_help, sonarr_data.main_tabs.tabs[5].contextual_help.unwrap(),
Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)) &INDEXERS_CONTEXT_CLUES
); );
assert_eq!(sonarr_data.main_tabs.tabs[5].config, None); assert_eq!(sonarr_data.main_tabs.tabs[5].config, None);
@@ -195,10 +195,10 @@ mod tests {
sonarr_data.main_tabs.tabs[6].route, sonarr_data.main_tabs.tabs[6].route,
ActiveSonarrBlock::System.into() 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!( assert_eq!(
sonarr_data.main_tabs.tabs[6].contextual_help, sonarr_data.main_tabs.tabs[6].contextual_help.unwrap(),
Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)) &SYSTEM_CONTEXT_CLUES
); );
assert_eq!(sonarr_data.main_tabs.tabs[6].config, None); assert_eq!(sonarr_data.main_tabs.tabs[6].config, None);
@@ -209,10 +209,14 @@ mod tests {
sonarr_data.series_info_tabs.tabs[0].route, sonarr_data.series_info_tabs.tabs[0].route,
ActiveSonarrBlock::SeriesDetails.into() 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!( assert_eq!(
sonarr_data.series_info_tabs.tabs[0].contextual_help, sonarr_data.series_info_tabs.tabs[0]
Some(build_context_clue_string(&SERIES_DETAILS_CONTEXT_CLUES)) .contextual_help
.unwrap(),
&SERIES_DETAILS_CONTEXT_CLUES
); );
assert_eq!(sonarr_data.series_info_tabs.tabs[0].config, None); 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, sonarr_data.series_info_tabs.tabs[1].route,
ActiveSonarrBlock::SeriesHistory.into() 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!( assert_eq!(
sonarr_data.series_info_tabs.tabs[1].contextual_help, sonarr_data.series_info_tabs.tabs[1]
Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES)) .contextual_help
.unwrap(),
&SERIES_HISTORY_CONTEXT_CLUES
); );
assert_eq!(sonarr_data.series_info_tabs.tabs[1].config, None); assert_eq!(sonarr_data.series_info_tabs.tabs[1].config, None);
} }
+7
View File
@@ -107,6 +107,13 @@ pub struct EditIndexerParams {
pub clear_tags: bool, 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)] #[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct HostConfig { pub struct HostConfig {
+37 -9
View File
@@ -4,15 +4,16 @@ use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::style::{Style, Stylize}; use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Text}; use ratatui::text::{Line, Text};
use ratatui::widgets::Clear;
use ratatui::widgets::Paragraph; use ratatui::widgets::Paragraph;
use ratatui::widgets::Tabs; use ratatui::widgets::Tabs;
use ratatui::widgets::Wrap; use ratatui::widgets::Wrap;
use ratatui::widgets::{Clear, Row};
use ratatui::Frame; use ratatui::Frame;
use sonarr_ui::SonarrUi; use sonarr_ui::SonarrUi;
use utils::layout_block; use utils::layout_block;
use crate::app::App; use crate::app::App;
use crate::models::servarr_models::KeybindingItem;
use crate::models::{HorizontallyScrollableText, Route, TabState}; use crate::models::{HorizontallyScrollableText, Route, TabState};
use crate::ui::radarr_ui::RadarrUi; use crate::ui::radarr_ui::RadarrUi;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
@@ -22,6 +23,7 @@ use crate::ui::utils::{
unstyled_title_block, unstyled_title_block,
}; };
use crate::ui::widgets::input_box::InputBox; use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::Size; use crate::ui::widgets::popup::Size;
mod builtin_themes; 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) { 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) .flex(Flex::SpaceBetween)
.margin(1) .margin(1)
.areas(area); .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 let titles = app
.server_tabs .server_tabs
@@ -138,6 +144,34 @@ pub fn draw_popup(
popup_fn(f, app, popup_area); 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 { fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -> Rect {
if title.is_empty() { if title.is_empty() {
f.render_widget(layout_block().default(), area); 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)]) let [header_area, content_area] = Layout::vertical([Constraint::Length(1), Constraint::Fill(0)])
.margin(1) .margin(1)
.areas(area); .areas(area);
let [tabs_area, help_area] =
Layout::horizontal([Constraint::Percentage(45), Constraint::Fill(0)]).areas(header_area);
let titles = tab_state let titles = tab_state
.tabs .tabs
@@ -159,12 +191,8 @@ fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -
.block(borderless_block()) .block(borderless_block())
.highlight_style(Style::new().secondary()) .highlight_style(Style::new().secondary())
.select(tab_state.index); .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(tabs, header_area);
f.render_widget(help, help_area);
content_area content_area
} }
-6
View File
@@ -82,11 +82,6 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
app.data.radarr_data.blocklist.current_selection().clone() 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 blocklist_row_mapping = |blocklist_item: &BlocklistItem| {
let BlocklistItem { let BlocklistItem {
@@ -136,7 +131,6 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(blocklist_table_footer)
.sorting(active_radarr_block == ActiveRadarrBlock::BlocklistSortPrompt) .sorting(active_radarr_block == ActiveRadarrBlock::BlocklistSortPrompt)
.headers([ .headers([
"Movie Title", "Movie Title",
@@ -4,8 +4,6 @@ use ratatui::text::{Line, Text};
use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
use ratatui::Frame; 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::app::App;
use crate::models::radarr_models::CollectionMovie; use crate::models::radarr_models::CollectionMovie;
use crate::models::servarr_data::radarr::radarr_data::{ 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" "No"
}; };
let minimum_availability = collection_selection.minimum_availability.to_display_str(); 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![ let collection_description = Text::from(vec![
Line::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"))) .block(layout_block_top_border_with_title(title_style("Movies")))
.loading(app.is_loading) .loading(app.is_loading)
.footer_alignment(Alignment::Center) .footer_alignment(Alignment::Center)
.footer(Some(help_footer))
.headers([ .headers([
"", "",
"Title", "Title",
@@ -220,11 +213,10 @@ fn draw_movie_overview(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let title_block = title_block("Overview"); let title_block = title_block("Overview");
f.render_widget(title_block, area); f.render_widget(title_block, area);
let [paragraph_area, help_area] = let [paragraph_area] = Layout::vertical([Constraint::Percentage(95)])
Layout::vertical([Constraint::Percentage(95), Constraint::Length(1)]) .flex(Flex::SpaceBetween)
.flex(Flex::SpaceBetween) .margin(1)
.margin(1) .areas(area);
.areas(area);
let overview = Text::from( let overview = Text::from(
app app
.data .data
@@ -235,15 +227,10 @@ fn draw_movie_overview(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.overview, .overview,
) )
.default(); .default();
let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help());
let paragraph = Paragraph::new(overview) let paragraph = Paragraph::new(overview)
.block(borderless_block()) .block(borderless_block())
.wrap(Wrap { trim: false }); .wrap(Wrap { trim: false });
let help_paragraph = Paragraph::new(help_text)
.block(borderless_block())
.centered();
f.render_widget(paragraph, paragraph_area); f.render_widget(paragraph, paragraph_area);
f.render_widget(help_paragraph, help_area);
} }
@@ -1,10 +1,8 @@
use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::Text; use ratatui::widgets::ListItem;
use ratatui::widgets::{ListItem, Paragraph};
use ratatui::Frame; use ratatui::Frame;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES};
use crate::app::App; use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditCollectionModal; use crate::models::servarr_data::radarr::modals::EditCollectionModal;
use crate::models::servarr_data::radarr::radarr_data::{ 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_minimum_availability = minimum_availability_list.current_selection();
let selected_quality_profile = quality_profile_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([ Layout::vertical([
Constraint::Length(6), Constraint::Length(6),
Constraint::Length(3), Constraint::Length(3),
@@ -112,7 +110,6 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
Constraint::Length(3), Constraint::Length(3),
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(1),
]) ])
.margin(1) .margin(1)
.areas(area); .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)]) Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(buttons_area); .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 prompt_paragraph = layout_paragraph_borderless(&collection_overview);
let monitored_checkbox = Checkbox::new("Monitored") let monitored_checkbox = Checkbox::new("Monitored")
.highlighted(selected_block == ActiveRadarrBlock::EditCollectionToggleMonitored) .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(search_on_add_checkbox, search_on_add_area);
f.render_widget(save_button, save_area); f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_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<'_>) { fn draw_edit_collection_select_minimum_availability_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
-6
View File
@@ -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 quality_profile_map = &app.data.radarr_data.quality_profile_map;
let content = Some(&mut app.data.radarr_data.collections); 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 collection_row_mapping = |collection: &Collection| {
let number_of_movies = collection.movies.as_ref().unwrap_or(&Vec::new()).len(); let number_of_movies = collection.movies.as_ref().unwrap_or(&Vec::new()).len();
collection.title.scroll_left_or_reset( 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.movies.is_empty()
|| app.data.radarr_data.quality_profile_map.is_empty(), || app.data.radarr_data.quality_profile_map.is_empty(),
) )
.footer(collections_table_footer)
.block(layout_block_top_border()) .block(layout_block_top_border())
.sorting(active_radarr_block == ActiveRadarrBlock::CollectionsSortPrompt) .sorting(active_radarr_block == ActiveRadarrBlock::CollectionsSortPrompt)
.searching(active_radarr_block == ActiveRadarrBlock::SearchCollection) .searching(active_radarr_block == ActiveRadarrBlock::SearchCollection)
-6
View File
@@ -72,11 +72,6 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
app.data.radarr_data.downloads.current_selection().clone() 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 downloads_row_mapping = |download_record: &DownloadRecord| {
let DownloadRecord { let DownloadRecord {
@@ -125,7 +120,6 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(downloads_table_footer)
.headers([ .headers([
"Title", "Title",
"Percent Complete", "Percent Complete",
+4 -15
View File
@@ -1,6 +1,5 @@
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES};
use crate::app::App; use crate::app::App;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::Route; use crate::models::Route;
@@ -14,8 +13,6 @@ use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::popup::Size; use crate::ui::widgets::popup::Size;
use crate::ui::{draw_popup, DrawUi}; use crate::ui::{draw_popup, DrawUi};
use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::Paragraph;
use ratatui::Frame; use ratatui::Frame;
#[cfg(test)] #[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 highlight_yes_no = selected_block == ActiveRadarrBlock::EditIndexerConfirmPrompt;
let edit_indexer_modal_option = &app.data.radarr_data.edit_indexer_modal; let edit_indexer_modal_option = &app.data.radarr_data.edit_indexer_modal;
let protocol = &app.data.radarr_data.indexers.current_selection().protocol; 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() { if edit_indexer_modal_option.is_some() {
f.render_widget(block, area); f.render_widget(block, area);
let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap(); let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap();
let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([ let [settings_area, buttons_area] =
Constraint::Fill(1), Layout::vertical([Constraint::Fill(1), Constraint::Length(3)])
Constraint::Length(18), .margin(1)
Constraint::Fill(1), .areas(area);
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
let [left_side_area, right_side_area] = let [left_side_area, right_side_area] =
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.margin(1) .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(interactive_search_checkbox, interactive_search_area);
f.render_widget(save_button, save_area); f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area); f.render_widget(cancel_button, cancel_area);
f.render_widget(help_paragraph, help_area);
} }
} else { } else {
f.render_widget(LoadingBlock::new(app.is_loading, block), area); f.render_widget(LoadingBlock::new(app.is_loading, block), area);
@@ -1,11 +1,8 @@
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::Paragraph;
use ratatui::Frame; use ratatui::Frame;
use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES};
use crate::app::App; use crate::app::App;
use crate::models::servarr_data::radarr::radarr_data::{ use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS, 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 selected_block = app.data.radarr_data.selected_block.get_active_block();
let highlight_yes_no = selected_block == ActiveRadarrBlock::IndexerSettingsConfirmPrompt; let highlight_yes_no = selected_block == ActiveRadarrBlock::IndexerSettingsConfirmPrompt;
let indexer_settings_option = &app.data.radarr_data.indexer_settings; 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() { if indexer_settings_option.is_some() {
f.render_widget(block, area); f.render_widget(block, area);
let indexer_settings = indexer_settings_option.as_ref().unwrap(); let indexer_settings = indexer_settings_option.as_ref().unwrap();
let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([ let [settings_area, buttons_area] =
Constraint::Fill(1), Layout::vertical([Constraint::Fill(1), Constraint::Length(3)])
Constraint::Length(15), .margin(1)
Constraint::Fill(1), .areas(area);
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
let [left_side_area, right_side_area] = let [left_side_area, right_side_area] =
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.margin(1) .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(allow_hardcoded_subs_checkbox, allow_hardcoded_subs_area);
f.render_widget(save_button, save_area); f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area); f.render_widget(cancel_button, cancel_area);
f.render_widget(help_paragraph, help_area);
} else { } else {
f.render_widget(LoadingBlock::new(app.is_loading, block), area); f.render_widget(LoadingBlock::new(app.is_loading, block), area);
} }
-6
View File
@@ -157,17 +157,11 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let indexers_table_footer = app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help();
let indexers_table = ManagarrTable::new( let indexers_table = ManagarrTable::new(
Some(&mut app.data.radarr_data.indexers), Some(&mut app.data.radarr_data.indexers),
indexers_row_mapping, indexers_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.footer(indexers_table_footer)
.loading(app.is_loading) .loading(app.is_loading)
.headers([ .headers([
"Indexer", "Indexer",
@@ -1,4 +1,3 @@
use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES};
use crate::app::App; use crate::app::App;
use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; 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() IndexerTestResultModalItem::default()
}; };
f.render_widget(block, area); 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| { let test_results_row_mapping = |result: &IndexerTestResultModalItem| {
result.validation_failures.scroll_left_or_reset( result.validation_failures.scroll_left_or_reset(
get_width_from_percentage(area, 86), 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, test_results_row_mapping,
) )
.loading(is_loading) .loading(is_loading)
.footer(Some(help_footer))
.footer_alignment(Alignment::Center) .footer_alignment(Alignment::Center)
.margin(1) .margin(1)
.headers(["Indexer", "Pass/Fail", "Failure Messages"]) .headers(["Indexer", "Pass/Fail", "Failure Messages"])
+7 -36
View File
@@ -1,14 +1,9 @@
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::Text; use ratatui::widgets::{Cell, ListItem, Row};
use ratatui::widgets::{Cell, ListItem, Paragraph, Row};
use ratatui::Frame; 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::radarr_models::AddMovieSearchResult;
use crate::models::servarr_data::radarr::modals::AddMovieModal; use crate::models::servarr_data::radarr::modals::AddMovieModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS}; 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::radarr_ui::collections::CollectionsUi;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{ use crate::ui::utils::{
borderless_block, get_width_from_percentage, layout_block, layout_paragraph_borderless, get_width_from_percentage, layout_block, layout_paragraph_borderless, title_block_centered,
title_block_centered,
}; };
use crate::ui::widgets::button::Button; use crate::ui::widgets::button::Button;
use crate::ui::widgets::input_box::InputBox; 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() AddMovieSearchResult::default()
}; };
let [search_box_area, results_area, help_area] = Layout::vertical([ let [search_box_area, results_area] =
Constraint::Length(3), Layout::vertical([Constraint::Length(3), Constraint::Fill(0)])
Constraint::Fill(0), .margin(1)
Constraint::Length(3), .areas(area);
])
.margin(1)
.areas(area);
let block_content = &app.data.radarr_data.add_movie_search.as_ref().unwrap().text; let block_content = &app.data.radarr_data.add_movie_search.as_ref().unwrap().text;
let offset = app let offset = app
.data .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) let search_box = InputBox::new(block_content)
.offset(offset) .offset(offset)
.block(title_block_centered("Add Movie")); .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); search_box.show_cursor(f, search_box_area);
f.render_widget(layout_block().default(), results_area); f.render_widget(layout_block().default(), results_area);
f.render_widget(search_box, search_box_area); f.render_widget(search_box, search_box_area);
f.render_widget(help_paragraph, help_area);
} }
ActiveRadarrBlock::AddMovieEmptySearchResults => { 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 = Message::new("No movies found matching your query!");
let error_message_popup = Popup::new(error_message).size(Size::Message); let error_message_popup = Popup::new(error_message).size(Size::Message);
f.render_widget(layout_block().default(), results_area); f.render_widget(layout_block().default(), results_area);
f.render_widget(error_message_popup, f.area()); f.render_widget(error_message_popup, f.area());
f.render_widget(help_paragraph, help_area);
} }
ActiveRadarrBlock::AddMovieSearchResults ActiveRadarrBlock::AddMovieSearchResults
| ActiveRadarrBlock::AddMoviePrompt | ActiveRadarrBlock::AddMoviePrompt
@@ -194,11 +175,6 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
| ActiveRadarrBlock::AddMovieSelectRootFolder | ActiveRadarrBlock::AddMovieSelectRootFolder
| ActiveRadarrBlock::AddMovieAlreadyInLibrary | ActiveRadarrBlock::AddMovieAlreadyInLibrary
| ActiveRadarrBlock::AddMovieTagsInput => { | 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( let search_results_table = ManagarrTable::new(
app.data.radarr_data.add_searched_movies.as_mut(), app.data.radarr_data.add_searched_movies.as_mut(),
search_results_row_mapping, 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(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); 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([ Layout::vertical([
Constraint::Length(6), Constraint::Length(6),
Constraint::Length(3), Constraint::Length(3),
@@ -332,16 +307,12 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Constraint::Length(3), Constraint::Length(3),
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(1),
]) ])
.margin(1) .margin(1)
.areas(area); .areas(area);
let prompt_paragraph = layout_paragraph_borderless(&prompt); 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(prompt_paragraph, paragraph_area);
f.render_widget(help_paragraph, help_area);
let [add_area, cancel_area] = let [add_area, cancel_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
+2 -8
View File
@@ -2,11 +2,9 @@ use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Rect}; use ratatui::layout::{Constraint, Rect};
use ratatui::prelude::Layout; use ratatui::prelude::Layout;
use ratatui::text::Text; use ratatui::widgets::ListItem;
use ratatui::widgets::{ListItem, Paragraph};
use ratatui::Frame; use ratatui::Frame;
use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES};
use crate::app::App; use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditMovieModal; use crate::models::servarr_data::radarr::modals::EditMovieModal;
use crate::models::servarr_data::radarr::radarr_data::{ 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_minimum_availability = minimum_availability_list.current_selection();
let selected_quality_profile = quality_profile_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([ Layout::vertical([
Constraint::Length(6), Constraint::Length(6),
Constraint::Length(3), Constraint::Length(3),
@@ -104,7 +102,6 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
Constraint::Length(3), Constraint::Length(3),
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(1),
]) ])
.margin(1) .margin(1)
.areas(area); .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)]) Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(buttons_area); .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 prompt_paragraph = layout_paragraph_borderless(&movie_overview);
let monitored_checkbox = Checkbox::new("Monitored") let monitored_checkbox = Checkbox::new("Monitored")
.checked(monitored.unwrap_or_default()) .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(quality_profile_drop_down_button, quality_profile_area);
f.render_widget(save_button, save_area); f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_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<'_>) { fn draw_edit_movie_select_minimum_availability_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
-6
View File
@@ -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 tags_map = &app.data.radarr_data.tags_map;
let downloads_vec = &app.data.radarr_data.downloads.items; let downloads_vec = &app.data.radarr_data.downloads.items;
let content = Some(&mut app.data.radarr_data.movies); 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| { let library_table_row_mapping = |movie: &Movie| {
movie.title.scroll_left_or_reset( 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) let library_table = ManagarrTable::new(content, library_table_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(help_footer)
.sorting(active_radarr_block == ActiveRadarrBlock::MoviesSortPrompt) .sorting(active_radarr_block == ActiveRadarrBlock::MoviesSortPrompt)
.searching(active_radarr_block == ActiveRadarrBlock::SearchMovie) .searching(active_radarr_block == ActiveRadarrBlock::SearchMovie)
.search_produced_empty_results(active_radarr_block == ActiveRadarrBlock::SearchMovieError) .search_produced_empty_results(active_radarr_block == ActiveRadarrBlock::SearchMovieError)
+1 -25
View File
@@ -259,18 +259,12 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let help_footer = app
.data
.radarr_data
.movie_info_tabs
.get_active_tab_contextual_help();
let history_table = ManagarrTable::new( let history_table = ManagarrTable::new(
Some(&mut movie_details_modal.movie_history), Some(&mut movie_details_modal.movie_history),
history_row_mapping, history_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(help_footer)
.headers(["Source Title", "Event Type", "Languages", "Quality", "Date"]) .headers(["Source Title", "Event Type", "Languages", "Quality", "Date"])
.constraints([ .constraints([
Constraint::Percentage(34), Constraint::Percentage(34),
@@ -301,14 +295,8 @@ fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.primary() .primary()
}; };
let content = Some(&mut movie_details_modal.movie_cast); 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) let cast_table = ManagarrTable::new(content, cast_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.footer(help_footer)
.loading(app.is_loading) .loading(app.is_loading)
.headers(["Cast Member", "Character"]) .headers(["Cast Member", "Character"])
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); .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() .primary()
}; };
let content = Some(&mut movie_details_modal.movie_crew); 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) let crew_table = ManagarrTable::new(content, crew_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.headers(["Crew Member", "Job", "Department"]) .headers(["Crew Member", "Job", "Department"])
.constraints(iter::repeat_n(Constraint::Ratio(1, 3), 3)) .constraints(iter::repeat_n(Constraint::Ratio(1, 3), 3));
.footer(help_footer);
f.render_widget(crew_table, area); 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 current_route = app.get_current_route();
let mut default_movie_details_modal = MovieDetailsModal::default(); 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( let content = Some(
&mut app &mut app
.data .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) let releases_table = ManagarrTable::new(content, releases_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading || is_empty) .loading(app.is_loading || is_empty)
.footer(help_footer)
.sorting(active_radarr_block == ActiveRadarrBlock::ManualSearchSortPrompt) .sorting(active_radarr_block == ActiveRadarrBlock::ManualSearchSortPrompt)
.headers([ .headers([
"Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality", "Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality",
-6
View File
@@ -59,11 +59,6 @@ impl DrawUi for RootFoldersUi {
} }
fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { 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 root_folders_row_mapping = |root_folders: &RootFolder| {
let RootFolder { let RootFolder {
path, path,
@@ -94,7 +89,6 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(help_footer)
.headers(["Path", "Free Space", "Unmapped Folders"]) .headers(["Path", "Free Space", "Unmapped Folders"])
.constraints([ .constraints([
Constraint::Ratio(3, 5), Constraint::Ratio(3, 5),
+4 -31
View File
@@ -4,7 +4,7 @@ use chrono::Utc;
use ratatui::layout::Layout; use ratatui::layout::Layout;
use ratatui::style::Style; use ratatui::style::Style;
use ratatui::text::{Span, Text}; use ratatui::text::{Span, Text};
use ratatui::widgets::{Cell, Paragraph, Row}; use ratatui::widgets::{Cell, Row};
use ratatui::{ use ratatui::{
layout::{Constraint, Rect}, layout::{Constraint, Rect},
widgets::ListItem, widgets::ListItem,
@@ -17,9 +17,7 @@ use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_models::QueueEvent; use crate::models::servarr_models::QueueEvent;
use crate::ui::radarr_ui::system::system_details_ui::SystemDetailsUi; use crate::ui::radarr_ui::system::system_details_ui::SystemDetailsUi;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{ use crate::ui::utils::{convert_to_minutes_hours_days, style_log_list_item};
convert_to_minutes_hours_days, layout_block_top_border, style_log_list_item,
};
use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::selectable_list::SelectableList; 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) { fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let [activities_area, logs_area, help_area] = Layout::vertical([ let [activities_area, logs_area] =
Constraint::Ratio(1, 2), Layout::vertical([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(area);
Constraint::Ratio(1, 2),
Constraint::Min(2),
])
.areas(area);
let [tasks_area, events_area] = let [tasks_area, events_area] =
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(activities_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_tasks(f, app, tasks_area);
draw_queued_events(f, app, events_area); draw_queued_events(f, app, events_area);
draw_logs(f, app, logs_area); draw_logs(f, app, logs_area);
draw_help(f, app, help_area);
} }
fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { 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); 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) struct TaskProps {
pub(super) name: String, pub(super) name: String,
pub(super) interval: String, pub(super) interval: String,
+4 -28
View File
@@ -3,8 +3,6 @@ use ratatui::text::{Span, Text};
use ratatui::widgets::{Cell, ListItem, Paragraph, Row}; use ratatui::widgets::{Cell, ListItem, Paragraph, Row};
use ratatui::Frame; 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::app::App;
use crate::models::radarr_models::RadarrTask; use crate::models::radarr_models::RadarrTask;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS}; 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<'_>) { fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let block = title_block("Log Details"); 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() { if app.data.radarr_data.log_details.items.is_empty() {
let loading = LoadingBlock::new(app.is_loading, borderless_block()); let loading = LoadingBlock::new(app.is_loading, borderless_block());
let popup = Popup::new(loading) let popup = Popup::new(loading).size(Size::Large).block(block);
.size(Size::Large)
.block(block)
.footer(&help_footer);
f.render_widget(popup, f.area()); f.render_widget(popup, f.area());
return; 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) style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level)
}) })
.block(borderless_block()); .block(borderless_block());
let popup = Popup::new(logs_list) let popup = Popup::new(logs_list).size(Size::Large).block(block);
.size(Size::Large)
.block(block)
.footer(&help_footer);
f.render_widget(popup, f.area()); f.render_widget(popup, f.area());
} }
fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { 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 tasks_row_mapping = |task: &RadarrTask| {
let task_props = extract_task_props(task); 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) let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping)
.loading(app.is_loading) .loading(app.is_loading)
.margin(1) .margin(1)
.footer(help_footer)
.footer_alignment(Alignment::Center) .footer_alignment(Alignment::Center)
.headers(TASK_TABLE_HEADERS) .headers(TASK_TABLE_HEADERS)
.constraints(TASK_TABLE_CONSTRAINTS); .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<'_>) { 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 updates = app.data.radarr_data.updates.get_text();
let block = title_block("Updates"); 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)) let updates_paragraph = Paragraph::new(Text::from(updates))
.block(borderless_block()) .block(borderless_block())
.scroll((app.data.radarr_data.updates.offset, 0)); .scroll((app.data.radarr_data.updates.offset, 0));
let popup = Popup::new(updates_paragraph) let popup = Popup::new(updates_paragraph).size(Size::Large).block(block);
.size(Size::Large)
.block(block)
.footer(&help_footer);
f.render_widget(popup, f.area()); f.render_widget(popup, f.area());
} else { } else {
let loading = LoadingBlock::new(app.is_loading, borderless_block()); let loading = LoadingBlock::new(app.is_loading, borderless_block());
let popup = Popup::new(loading) let popup = Popup::new(loading).size(Size::Large).block(block);
.size(Size::Large)
.block(block)
.footer(&help_footer);
f.render_widget(popup, f.area()); f.render_widget(popup, f.area());
} }
-7
View File
@@ -77,12 +77,6 @@ impl DrawUi for BlocklistUi {
fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { 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 blocklist_row_mapping = |blocklist_item: &BlocklistItem| {
let BlocklistItem { let BlocklistItem {
source_title, source_title,
@@ -115,7 +109,6 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(blocklist_table_footer)
.sorting(active_sonarr_block == ActiveSonarrBlock::BlocklistSortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::BlocklistSortPrompt)
.headers([ .headers([
"Series Title", "Series Title",
-6
View File
@@ -72,11 +72,6 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else { } else {
app.data.sonarr_data.downloads.current_selection().clone() 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 downloads_row_mapping = |download_record: &DownloadRecord| {
let DownloadRecord { let DownloadRecord {
@@ -130,7 +125,6 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(downloads_table_footer)
.headers([ .headers([
"Title", "Title",
"Percent Complete", "Percent Complete",
-7
View File
@@ -55,12 +55,6 @@ fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
app.data.sonarr_data.history.current_selection().clone() app.data.sonarr_data.history.current_selection().clone()
}; };
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { 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 history_row_mapping = |history_item: &SonarrHistoryItem| {
let SonarrHistoryItem { let SonarrHistoryItem {
source_title, 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) ManagarrTable::new(Some(&mut app.data.sonarr_data.history), history_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(history_table_footer)
.sorting(active_sonarr_block == ActiveSonarrBlock::HistorySortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::HistorySortPrompt)
.searching(active_sonarr_block == ActiveSonarrBlock::SearchHistory) .searching(active_sonarr_block == ActiveSonarrBlock::SearchHistory)
.search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchHistoryError) .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchHistoryError)
+4 -15
View File
@@ -1,6 +1,5 @@
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES};
use crate::app::App; use crate::app::App;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::Route; use crate::models::Route;
@@ -14,8 +13,6 @@ use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::popup::Size; use crate::ui::widgets::popup::Size;
use crate::ui::{draw_popup, DrawUi}; use crate::ui::{draw_popup, DrawUi};
use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::Paragraph;
use ratatui::Frame; use ratatui::Frame;
#[cfg(test)] #[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 highlight_yes_no = selected_block == ActiveSonarrBlock::EditIndexerConfirmPrompt;
let edit_indexer_modal_option = &app.data.sonarr_data.edit_indexer_modal; let edit_indexer_modal_option = &app.data.sonarr_data.edit_indexer_modal;
let protocol = &app.data.sonarr_data.indexers.current_selection().protocol; 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() { if edit_indexer_modal_option.is_some() {
f.render_widget(block, area); f.render_widget(block, area);
let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap(); let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap();
let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([ let [settings_area, buttons_area] =
Constraint::Fill(1), Layout::vertical([Constraint::Fill(1), Constraint::Length(3)])
Constraint::Length(18), .margin(1)
Constraint::Fill(1), .areas(area);
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.areas(area);
let [left_side_area, right_side_area] = let [left_side_area, right_side_area] =
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.margin(1) .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(interactive_search_checkbox, interactive_search_area);
f.render_widget(save_button, save_area); f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area); f.render_widget(cancel_button, cancel_area);
f.render_widget(help_paragraph, help_area);
} }
} else { } else {
f.render_widget(LoadingBlock::new(app.is_loading, block), area); f.render_widget(LoadingBlock::new(app.is_loading, block), area);
@@ -1,9 +1,6 @@
use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::Paragraph;
use ratatui::Frame; use ratatui::Frame;
use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES};
use crate::app::App; use crate::app::App;
use crate::models::servarr_data::sonarr::sonarr_data::{ use crate::models::servarr_data::sonarr::sonarr_data::{
ActiveSonarrBlock, INDEXER_SETTINGS_BLOCKS, 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 selected_block = app.data.sonarr_data.selected_block.get_active_block();
let highlight_yes_no = selected_block == ActiveSonarrBlock::IndexerSettingsConfirmPrompt; let highlight_yes_no = selected_block == ActiveSonarrBlock::IndexerSettingsConfirmPrompt;
let indexer_settings_option = &app.data.sonarr_data.indexer_settings; 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() { if indexer_settings_option.is_some() {
f.render_widget(block, area); f.render_widget(block, area);
let indexer_settings = indexer_settings_option.as_ref().unwrap(); 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([ Layout::vertical([
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(3), Constraint::Length(3),
@@ -60,7 +55,6 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area:
Constraint::Length(3), Constraint::Length(3),
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(1),
]) ])
.margin(1) .margin(1)
.areas(area); .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(save_button, save_area);
f.render_widget(cancel_button, cancel_area); f.render_widget(cancel_button, cancel_area);
f.render_widget(help_paragraph, help_area);
} else { } else {
f.render_widget(LoadingBlock::new(app.is_loading, block), area); f.render_widget(LoadingBlock::new(app.is_loading, block), area);
} }
-6
View File
@@ -157,17 +157,11 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
]) ])
.primary() .primary()
}; };
let indexers_table_footer = app
.data
.sonarr_data
.main_tabs
.get_active_tab_contextual_help();
let indexers_table = ManagarrTable::new( let indexers_table = ManagarrTable::new(
Some(&mut app.data.sonarr_data.indexers), Some(&mut app.data.sonarr_data.indexers),
indexers_row_mapping, indexers_row_mapping,
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.footer(indexers_table_footer)
.loading(app.is_loading) .loading(app.is_loading)
.headers([ .headers([
"Indexer", "Indexer",
@@ -1,4 +1,3 @@
use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES};
use crate::app::App; use crate::app::App;
use crate::models::servarr_data::modals::IndexerTestResultModalItem; use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; 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() IndexerTestResultModalItem::default()
}; };
f.render_widget(title_block("Test All Indexers"), area); 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| { let test_results_row_mapping = |result: &IndexerTestResultModalItem| {
result.validation_failures.scroll_left_or_reset( result.validation_failures.scroll_left_or_reset(
get_width_from_percentage(area, 86), 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, test_results_row_mapping,
) )
.loading(is_loading) .loading(is_loading)
.footer(Some(help_footer))
.footer_alignment(Alignment::Center) .footer_alignment(Alignment::Center)
.margin(1) .margin(1)
.headers(["Indexer", "Pass/Fail", "Failure Messages"]) .headers(["Indexer", "Pass/Fail", "Failure Messages"])
+7 -36
View File
@@ -1,22 +1,16 @@
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::Text; use ratatui::widgets::{Cell, ListItem, Row};
use ratatui::widgets::{Cell, ListItem, Paragraph, Row};
use ratatui::Frame; 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::modals::AddSeriesModal;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS};
use crate::models::sonarr_models::AddSeriesSearchResult; use crate::models::sonarr_models::AddSeriesSearchResult;
use crate::models::Route; use crate::models::Route;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{ use crate::ui::utils::{
borderless_block, get_width_from_percentage, layout_block, layout_paragraph_borderless, get_width_from_percentage, layout_block, layout_paragraph_borderless, title_block_centered,
title_block_centered,
}; };
use crate::ui::widgets::button::Button; use crate::ui::widgets::button::Button;
use crate::ui::widgets::checkbox::Checkbox; 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() AddSeriesSearchResult::default()
}; };
let [search_box_area, results_area, help_area] = Layout::vertical([ let [search_box_area, results_area] =
Constraint::Length(3), Layout::vertical([Constraint::Length(3), Constraint::Fill(0)])
Constraint::Fill(0), .margin(1)
Constraint::Length(3), .areas(area);
])
.margin(1)
.areas(area);
let block_content = &app let block_content = &app
.data .data
.sonarr_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) let search_box = InputBox::new(block_content)
.offset(offset) .offset(offset)
.block(title_block_centered("Add Series")); .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); search_box.show_cursor(f, search_box_area);
f.render_widget(layout_block().default(), results_area); f.render_widget(layout_block().default(), results_area);
f.render_widget(search_box, search_box_area); f.render_widget(search_box, search_box_area);
f.render_widget(help_paragraph, help_area);
} }
ActiveSonarrBlock::AddSeriesEmptySearchResults => { 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 = Message::new("No series found matching your query!");
let error_message_popup = Popup::new(error_message).size(Size::Message); let error_message_popup = Popup::new(error_message).size(Size::Message);
f.render_widget(layout_block().default(), results_area); f.render_widget(layout_block().default(), results_area);
f.render_widget(error_message_popup, f.area()); f.render_widget(error_message_popup, f.area());
f.render_widget(help_paragraph, help_area);
} }
ActiveSonarrBlock::AddSeriesSearchResults ActiveSonarrBlock::AddSeriesSearchResults
| ActiveSonarrBlock::AddSeriesPrompt | ActiveSonarrBlock::AddSeriesPrompt
@@ -181,11 +162,6 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
| ActiveSonarrBlock::AddSeriesSelectRootFolder | ActiveSonarrBlock::AddSeriesSelectRootFolder
| ActiveSonarrBlock::AddSeriesAlreadyInLibrary | ActiveSonarrBlock::AddSeriesAlreadyInLibrary
| ActiveSonarrBlock::AddSeriesTagsInput => { | 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( let search_results_table = ManagarrTable::new(
app.data.sonarr_data.add_searched_series.as_mut(), app.data.sonarr_data.add_searched_series.as_mut(),
search_results_row_mapping, 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(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); 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([ Layout::vertical([
Constraint::Length(6), Constraint::Length(6),
Constraint::Length(3), Constraint::Length(3),
@@ -309,16 +284,12 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Constraint::Length(3), Constraint::Length(3),
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(1),
]) ])
.margin(1) .margin(1)
.areas(area); .areas(area);
let prompt_paragraph = layout_paragraph_borderless(&prompt); 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(prompt_paragraph, paragraph_area);
f.render_widget(help_paragraph, help_area);
let [add_area, cancel_area] = let [add_area, cancel_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
+2 -8
View File
@@ -2,11 +2,9 @@ use std::sync::atomic::Ordering;
use ratatui::layout::{Constraint, Rect}; use ratatui::layout::{Constraint, Rect};
use ratatui::prelude::Layout; use ratatui::prelude::Layout;
use ratatui::text::Text; use ratatui::widgets::ListItem;
use ratatui::widgets::{ListItem, Paragraph};
use ratatui::Frame; use ratatui::Frame;
use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES};
use crate::app::App; use crate::app::App;
use crate::models::servarr_data::sonarr::modals::EditSeriesModal; use crate::models::servarr_data::sonarr::modals::EditSeriesModal;
use crate::models::servarr_data::sonarr::sonarr_data::{ 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_quality_profile = quality_profile_list.current_selection();
let selected_language_profile = language_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([ Layout::vertical([
Constraint::Length(6), Constraint::Length(6),
Constraint::Length(3), Constraint::Length(3),
@@ -119,7 +117,6 @@ fn draw_edit_series_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, ar
Constraint::Length(3), Constraint::Length(3),
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(1),
]) ])
.margin(1) .margin(1)
.areas(area); .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)]) Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(buttons_area); .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 prompt_paragraph = layout_paragraph_borderless(&series_overview);
let monitored_checkbox = Checkbox::new("Monitored") let monitored_checkbox = Checkbox::new("Monitored")
.checked(monitored.unwrap_or_default()) .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(series_type_drop_down_button, series_type_area);
f.render_widget(save_button, save_area); f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_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<'_>) { fn draw_edit_series_select_series_type_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
@@ -257,9 +257,6 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
.current_selection() .current_selection()
.clone() .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 history_row_mapping = |history_item: &SonarrHistoryItem| {
let 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) ManagarrTable::new(Some(&mut episode_history_table), history_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(episode_history_table_footer)
.headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) .headers(["Source Title", "Event Type", "Language", "Quality", "Date"])
.constraints([ .constraints([
Constraint::Percentage(40), 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(), 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() { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let episode_release_row_mapping = |release: &SonarrRelease| { 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()) .block(layout_block_top_border())
.loading(app.is_loading || is_empty) .loading(app.is_loading || is_empty)
.footer(episode_release_table_footer)
.sorting(active_sonarr_block == ActiveSonarrBlock::ManualEpisodeSearchSortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::ManualEpisodeSearchSortPrompt)
.headers([ .headers([
"Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality", "Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality",
-6
View File
@@ -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 language_profile_map = &app.data.sonarr_data.language_profiles_map;
let tags_map = &app.data.sonarr_data.tags_map; let tags_map = &app.data.sonarr_data.tags_map;
let content = Some(&mut app.data.sonarr_data.series); 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| { let series_table_row_mapping = |series: &Series| {
series.title.scroll_left_or_reset( 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) let series_table = ManagarrTable::new(content, series_table_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(help_footer)
.sorting(active_sonarr_block == ActiveSonarrBlock::SeriesSortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesSortPrompt)
.searching(active_sonarr_block == ActiveSonarrBlock::SearchSeries) .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeries)
.filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeries) .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeries)
@@ -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) { fn draw_episodes_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { 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 let episode_files = app
.data .data
.sonarr_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) let season_table = ManagarrTable::new(content, episode_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(help_footer)
.searching(is_searching) .searching(is_searching)
.search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchEpisodesError) .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchEpisodesError)
.headers([ .headers([
@@ -261,9 +252,6 @@ fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.current_selection() .current_selection()
.clone() .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() { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let history_row_mapping = |history_item: &SonarrHistoryItem| { 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) ManagarrTable::new(Some(&mut season_history_table), history_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(season_history_table_footer)
.sorting(active_sonarr_block == ActiveSonarrBlock::SeasonHistorySortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::SeasonHistorySortPrompt)
.searching(active_sonarr_block == ActiveSonarrBlock::SearchSeasonHistory) .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeasonHistory)
.search_produced_empty_results( .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(), 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() { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let season_release_row_mapping = |release: &SonarrRelease| { 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) ManagarrTable::new(Some(&mut season_release_table), season_release_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading || is_empty) .loading(app.is_loading || is_empty)
.footer(season_release_table_footer)
.sorting(active_sonarr_block == ActiveSonarrBlock::ManualSeasonSearchSortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::ManualSeasonSearchSortPrompt)
.headers([ .headers([
"Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality", "Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality",
@@ -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) { fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let content = Some(&mut app.data.sonarr_data.seasons); 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_row_mapping = |season: &Season| {
let Season { let Season {
title, 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) let season_table = ManagarrTable::new(content, season_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(help_footer)
.searching(is_searching) .searching(is_searching)
.search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeasonError) .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeasonError)
.headers(["Monitored", "Season", "Episode Count", "Size on Disk"]) .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 { } else {
series_history.current_selection().clone() 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() { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let history_row_mapping = |history_item: &SonarrHistoryItem| { 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) ManagarrTable::new(Some(&mut series_history_table), history_row_mapping)
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(series_history_table_footer)
.sorting(active_sonarr_block == ActiveSonarrBlock::SeriesHistorySortPrompt) .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesHistorySortPrompt)
.searching(active_sonarr_block == ActiveSonarrBlock::SearchSeriesHistory) .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeriesHistory)
.search_produced_empty_results( .search_produced_empty_results(
-6
View File
@@ -59,11 +59,6 @@ impl DrawUi for RootFoldersUi {
} }
fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { 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 root_folders_row_mapping = |root_folders: &RootFolder| {
let RootFolder { let RootFolder {
path, path,
@@ -94,7 +89,6 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
) )
.block(layout_block_top_border()) .block(layout_block_top_border())
.loading(app.is_loading) .loading(app.is_loading)
.footer(help_footer)
.headers(["Path", "Free Space", "Unmapped Folders"]) .headers(["Path", "Free Space", "Unmapped Folders"])
.constraints([ .constraints([
Constraint::Ratio(3, 5), Constraint::Ratio(3, 5),
+4 -31
View File
@@ -4,7 +4,7 @@ use chrono::Utc;
use ratatui::layout::Layout; use ratatui::layout::Layout;
use ratatui::style::Style; use ratatui::style::Style;
use ratatui::text::{Span, Text}; use ratatui::text::{Span, Text};
use ratatui::widgets::{Cell, Paragraph, Row}; use ratatui::widgets::{Cell, Row};
use ratatui::{ use ratatui::{
layout::{Constraint, Rect}, layout::{Constraint, Rect},
widgets::ListItem, widgets::ListItem,
@@ -17,9 +17,7 @@ use crate::models::servarr_models::QueueEvent;
use crate::models::sonarr_models::SonarrTask; use crate::models::sonarr_models::SonarrTask;
use crate::ui::sonarr_ui::system::system_details_ui::SystemDetailsUi; use crate::ui::sonarr_ui::system::system_details_ui::SystemDetailsUi;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{ use crate::ui::utils::{convert_to_minutes_hours_days, style_log_list_item};
convert_to_minutes_hours_days, layout_block_top_border, style_log_list_item,
};
use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::selectable_list::SelectableList; 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) { fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let [activities_area, logs_area, help_area] = Layout::vertical([ let [activities_area, logs_area] =
Constraint::Ratio(1, 2), Layout::vertical([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(area);
Constraint::Ratio(1, 2),
Constraint::Min(2),
])
.areas(area);
let [tasks_area, events_area] = let [tasks_area, events_area] =
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(activities_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_tasks(f, app, tasks_area);
draw_queued_events(f, app, events_area); draw_queued_events(f, app, events_area);
draw_logs(f, app, logs_area); draw_logs(f, app, logs_area);
draw_help(f, app, help_area);
} }
fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { 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); 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) struct TaskProps {
pub(super) name: String, pub(super) name: String,
pub(super) interval: String, pub(super) interval: String,
+4 -28
View File
@@ -3,8 +3,6 @@ use ratatui::text::{Span, Text};
use ratatui::widgets::{Cell, ListItem, Paragraph, Row}; use ratatui::widgets::{Cell, ListItem, Paragraph, Row};
use ratatui::Frame; 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::app::App;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SYSTEM_DETAILS_BLOCKS};
use crate::models::sonarr_models::SonarrTask; use crate::models::sonarr_models::SonarrTask;
@@ -59,17 +57,10 @@ impl DrawUi for SystemDetailsUi {
fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) { fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let block = title_block("Log Details"); 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() { if app.data.sonarr_data.log_details.items.is_empty() {
let loading = LoadingBlock::new(app.is_loading, borderless_block()); let loading = LoadingBlock::new(app.is_loading, borderless_block());
let popup = Popup::new(loading) let popup = Popup::new(loading).size(Size::Large).block(block);
.size(Size::Large)
.block(block)
.footer(&help_footer);
f.render_widget(popup, f.area()); f.render_widget(popup, f.area());
return; 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) style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level)
}) })
.block(borderless_block()); .block(borderless_block());
let popup = Popup::new(logs_list) let popup = Popup::new(logs_list).size(Size::Large).block(block);
.size(Size::Large)
.block(block)
.footer(&help_footer);
f.render_widget(popup, f.area()); f.render_widget(popup, f.area());
} }
fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { 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 tasks_row_mapping = |task: &SonarrTask| {
let task_props = extract_task_props(task); 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) let tasks_table = ManagarrTable::new(Some(&mut app.data.sonarr_data.tasks), tasks_row_mapping)
.loading(app.is_loading) .loading(app.is_loading)
.margin(1) .margin(1)
.footer(help_footer)
.footer_alignment(Alignment::Center) .footer_alignment(Alignment::Center)
.headers(TASK_TABLE_HEADERS) .headers(TASK_TABLE_HEADERS)
.constraints(TASK_TABLE_CONSTRAINTS); .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<'_>) { 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 updates = app.data.sonarr_data.updates.get_text();
let block = title_block("Updates"); 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)) let updates_paragraph = Paragraph::new(Text::from(updates))
.block(borderless_block()) .block(borderless_block())
.scroll((app.data.sonarr_data.updates.offset, 0)); .scroll((app.data.sonarr_data.updates.offset, 0));
let popup = Popup::new(updates_paragraph) let popup = Popup::new(updates_paragraph).size(Size::Large).block(block);
.size(Size::Large)
.block(block)
.footer(&help_footer);
f.render_widget(popup, f.area()); f.render_widget(popup, f.area());
} else { } else {
let loading = LoadingBlock::new(app.is_loading, borderless_block()); let loading = LoadingBlock::new(app.is_loading, borderless_block());
let popup = Popup::new(loading) let popup = Popup::new(loading).size(Size::Large).block(block);
.size(Size::Large)
.block(block)
.footer(&help_footer);
f.render_widget(popup, f.area()); f.render_widget(popup, f.area());
} }
+3 -18
View File
@@ -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::styles::ManagarrStyle;
use crate::ui::utils::{layout_paragraph_borderless, title_block_centered}; use crate::ui::utils::{layout_paragraph_borderless, title_block_centered};
use crate::ui::widgets::button::Button; use crate::ui::widgets::button::Button;
@@ -6,7 +5,6 @@ use crate::ui::widgets::checkbox::Checkbox;
use derive_setters::Setters; use derive_setters::Setters;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::text::Text;
use ratatui::widgets::{Paragraph, Widget}; use ratatui::widgets::{Paragraph, Widget};
use std::iter; use std::iter;
@@ -40,16 +38,12 @@ impl ConfirmationPrompt<'_> {
fn render_confirmation_prompt_with_checkboxes(self, area: Rect, buf: &mut Buffer) { fn render_confirmation_prompt_with_checkboxes(self, area: Rect, buf: &mut Buffer) {
title_block_centered(self.title).render(area, buf); 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 { if let Some(checkboxes) = self.checkboxes {
let mut constraints = vec![ let mut constraints = vec![
Constraint::Length(4), Constraint::Length(4),
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(1),
]; ];
constraints.splice( constraints.splice(
1..1, 1..1,
@@ -61,7 +55,6 @@ impl ConfirmationPrompt<'_> {
.areas(chunks[checkboxes.len() + 2]); .areas(chunks[checkboxes.len() + 2]);
layout_paragraph_borderless(self.prompt).render(chunks[0], buf); layout_paragraph_borderless(self.prompt).render(chunks[0], buf);
help_paragraph.render(chunks[checkboxes.len() + 3], buf);
checkboxes checkboxes
.into_iter() .into_iter()
@@ -83,38 +76,30 @@ impl ConfirmationPrompt<'_> {
fn render_confirmation_prompt(self, area: Rect, buf: &mut Buffer) { fn render_confirmation_prompt(self, area: Rect, buf: &mut Buffer) {
title_block_centered(self.title).render(area, buf); 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, 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(4),
Constraint::Length(7), Constraint::Length(7),
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(1),
]) ])
.margin(1) .margin(1)
.areas(area); .areas(area);
content_paragraph.render(content_area, buf); content_paragraph.render(content_area, buf);
help_paragraph.render(help_area, buf);
[prompt_area, buttons_area] [prompt_area, buttons_area]
} else { } else {
let [prompt_area, buttons_area, _, help_area] = Layout::vertical([ let [prompt_area, _, buttons_area] = Layout::vertical([
Constraint::Percentage(72), Constraint::Percentage(72),
Constraint::Length(3),
Constraint::Fill(0), Constraint::Fill(0),
Constraint::Min(1), Constraint::Length(3),
]) ])
.margin(1) .margin(1)
.flex(Flex::SpaceBetween) .flex(Flex::SpaceBetween)
.areas(area); .areas(area);
help_paragraph.render(help_area, buf);
[prompt_area, buttons_area] [prompt_area, buttons_area]
}; };
+5 -13
View File
@@ -1,8 +1,7 @@
use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{background_block, centered_rect};
use crate::ui::utils::{background_block, borderless_block, centered_rect};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Layout, Rect}; 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; use super::input_box::InputBox;
@@ -39,17 +38,10 @@ impl<'a> InputBoxPopup<'a> {
Clear.render(popup_area, buf); Clear.render(popup_area, buf);
background_block().render(popup_area, buf); background_block().render(popup_area, buf);
let [text_box_area, help_area] = let [text_box_area] = Layout::vertical([Constraint::Length(3)])
Layout::vertical([Constraint::Length(3), Constraint::Length(1)]) .margin(1)
.margin(1) .areas(popup_area);
.areas(popup_area);
self.input_box.render_ref(text_box_area, buf); self.input_box.render_ref(text_box_area, buf);
let help = Paragraph::new("<esc> cancel")
.help()
.centered()
.block(borderless_block());
help.render(help_area, buf);
} }
} }
+6 -29
View File
@@ -1,9 +1,7 @@
use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{background_block, centered_rect};
use crate::ui::utils::{background_block, centered_rect, layout_block_top_border};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::layout::Rect;
use ratatui::prelude::Text; use ratatui::widgets::{Block, Clear, Widget};
use ratatui::widgets::{Block, Clear, Paragraph, Widget};
#[cfg(test)] #[cfg(test)]
#[path = "popup_tests.rs"] #[path = "popup_tests.rs"]
@@ -25,6 +23,7 @@ pub enum Size {
XLarge, XLarge,
XXLarge, XXLarge,
Long, Long,
LongNarrowTable,
} }
impl Size { impl Size {
@@ -45,6 +44,7 @@ impl Size {
Size::XLarge => (83, 83), Size::XLarge => (83, 83),
Size::XXLarge => (90, 90), Size::XXLarge => (90, 90),
Size::Long => (65, 75), Size::Long => (65, 75),
Size::LongNarrowTable => (55, 85),
} }
} }
} }
@@ -54,7 +54,6 @@ pub struct Popup<'a, T: Widget> {
percent_x: u16, percent_x: u16,
percent_y: u16, percent_y: u16,
block: Option<Block<'a>>, block: Option<Block<'a>>,
footer: Option<&'a str>,
} }
impl<'a, T: Widget> Popup<'a, T> { impl<'a, T: Widget> Popup<'a, T> {
@@ -64,7 +63,6 @@ impl<'a, T: Widget> Popup<'a, T> {
percent_x: 0, percent_x: 0,
percent_y: 0, percent_y: 0,
block: None, block: None,
footer: None,
} }
} }
@@ -86,11 +84,6 @@ impl<'a, T: Widget> Popup<'a, T> {
self self
} }
pub fn footer(mut self, footer: &'a str) -> Self {
self.footer = Some(footer);
self
}
fn render_popup(self, area: Rect, buf: &mut Buffer) { fn render_popup(self, area: Rect, buf: &mut Buffer) {
let mut popup_area = centered_rect(self.percent_x, self.percent_y, area); let mut popup_area = centered_rect(self.percent_x, self.percent_y, area);
let height = if popup_area.height < 3 { let height = if popup_area.height < 3 {
@@ -109,23 +102,7 @@ impl<'a, T: Widget> Popup<'a, T> {
block.render(popup_area, buf); block.render(popup_area, buf);
} }
let content_area = if let Some(footer) = self.footer { self.widget.render(popup_area, buf);
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);
} }
} }
+1 -15
View File
@@ -21,6 +21,7 @@ mod tests {
assert_eq!(Size::XLarge.to_percent(), (83, 83)); assert_eq!(Size::XLarge.to_percent(), (83, 83));
assert_eq!(Size::XXLarge.to_percent(), (90, 90)); assert_eq!(Size::XXLarge.to_percent(), (90, 90));
assert_eq!(Size::Long.to_percent(), (65, 75)); assert_eq!(Size::Long.to_percent(), (65, 75));
assert_eq!(Size::LongNarrowTable.to_percent(), (55, 85));
} }
#[test] #[test]
@@ -31,7 +32,6 @@ mod tests {
assert_eq!(popup.percent_x, 0); assert_eq!(popup.percent_x, 0);
assert_eq!(popup.percent_y, 0); assert_eq!(popup.percent_y, 0);
assert_eq!(popup.block, None); assert_eq!(popup.block, None);
assert_eq!(popup.footer, None);
} }
#[test] #[test]
@@ -42,7 +42,6 @@ mod tests {
assert_eq!(popup.percent_y, 40); assert_eq!(popup.percent_y, 40);
assert_eq!(popup.widget, Block::new()); assert_eq!(popup.widget, Block::new());
assert_eq!(popup.block, None); assert_eq!(popup.block, None);
assert_eq!(popup.footer, None);
} }
#[test] #[test]
@@ -53,7 +52,6 @@ mod tests {
assert_eq!(popup.percent_y, 50); assert_eq!(popup.percent_y, 50);
assert_eq!(popup.widget, Block::new()); assert_eq!(popup.widget, Block::new());
assert_eq!(popup.block, None); assert_eq!(popup.block, None);
assert_eq!(popup.footer, None);
} }
#[test] #[test]
@@ -64,17 +62,5 @@ mod tests {
assert_eq!(popup.widget, Block::new()); assert_eq!(popup.widget, Block::new());
assert_eq!(popup.percent_x, 0); assert_eq!(popup.percent_x, 0);
assert_eq!(popup.percent_y, 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);
} }
} }