Added the ability to edit existing indexers with basic options, added a tags column to the indexers table, and fixed a bug in the counter fields that displayed the cursor next to the integer instead of on it to make understanding the counter easier. Also upgraded to confy v0.60.0 and rust version to 1.75

This commit is contained in:
2024-01-19 15:45:41 -07:00
parent 3d249cc51c
commit 2ec4472efc
29 changed files with 3513 additions and 362 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ pub static ROOT_FOLDERS_CONTEXT_CLUES: [ContextClue; 3] = [
pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
(DEFAULT_KEYBINDINGS.submit, "edit indexer"),
(
DEFAULT_KEYBINDINGS.settings,
DEFAULT_KEYBINDINGS.settings.desc,
+2 -2
View File
@@ -151,8 +151,8 @@ mod tests {
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc);
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "edit indexer");
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
+1 -4
View File
@@ -1,7 +1,4 @@
pub use self::{
input_event::{Events, InputEvent},
key::Key,
};
pub use self::key::Key;
pub mod input_event;
mod key;
@@ -0,0 +1,431 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys};
#[cfg(test)]
#[path = "edit_indexer_handler_tests.rs"]
mod edit_indexer_handler_tests;
pub(super) struct EditIndexerHandler<'a, 'b> {
key: &'a Key,
app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>,
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for EditIndexerHandler<'a, 'b> {
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool {
EDIT_INDEXER_BLOCKS.contains(active_block)
}
fn with(
key: &'a Key,
app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>,
) -> EditIndexerHandler<'a, 'b> {
EditIndexerHandler {
key,
app,
active_radarr_block: active_block,
_context,
}
}
fn get_key(&self) -> &Key {
self.key
}
fn handle_scroll_up(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::EditIndexerPrompt {
self.app.data.radarr_data.selected_block.previous();
}
}
fn handle_scroll_down(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::EditIndexerPrompt {
self.app.data.radarr_data.selected_block.next();
}
}
fn handle_home(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::EditIndexerNameInput => {
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.name
.scroll_home();
}
ActiveRadarrBlock::EditIndexerUrlInput => {
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.url
.scroll_home();
}
ActiveRadarrBlock::EditIndexerApiKeyInput => {
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.api_key
.scroll_home();
}
ActiveRadarrBlock::EditIndexerSeedRatioInput => {
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.seed_ratio
.scroll_home();
}
ActiveRadarrBlock::EditIndexerTagsInput => {
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.tags
.scroll_home();
}
_ => (),
}
}
fn handle_end(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::EditIndexerNameInput => {
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.name
.reset_offset();
}
ActiveRadarrBlock::EditIndexerUrlInput => {
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.url
.reset_offset();
}
ActiveRadarrBlock::EditIndexerApiKeyInput => {
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.api_key
.reset_offset();
}
ActiveRadarrBlock::EditIndexerSeedRatioInput => {
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.seed_ratio
.reset_offset();
}
ActiveRadarrBlock::EditIndexerTagsInput => {
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.tags
.reset_offset();
}
_ => (),
}
}
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::EditIndexerPrompt => {
if self.app.data.radarr_data.selected_block.get_active_block()
== &ActiveRadarrBlock::EditIndexerConfirmPrompt
{
handle_prompt_toggle(self.app, self.key);
} else {
let len = self.app.data.radarr_data.selected_block.blocks.len();
let idx = self.app.data.radarr_data.selected_block.index;
self.app.data.radarr_data.selected_block.index = (idx + 5) % len;
}
}
ActiveRadarrBlock::EditIndexerNameInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.name
);
}
ActiveRadarrBlock::EditIndexerUrlInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.url
);
}
ActiveRadarrBlock::EditIndexerApiKeyInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.api_key
);
}
ActiveRadarrBlock::EditIndexerSeedRatioInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.seed_ratio
);
}
ActiveRadarrBlock::EditIndexerTagsInput => {
handle_text_box_left_right_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.tags
);
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::EditIndexerPrompt => {
let selected_block = *self.app.data.radarr_data.selected_block.get_active_block();
match selected_block {
ActiveRadarrBlock::EditIndexerConfirmPrompt => {
let radarr_data = &mut self.app.data.radarr_data;
if radarr_data.prompt_confirm {
radarr_data.prompt_confirm_action = Some(RadarrEvent::EditIndexer);
self.app.should_refresh = true;
} else {
radarr_data.edit_indexer_modal = None;
}
self.app.pop_navigation_stack();
}
ActiveRadarrBlock::EditIndexerNameInput
| ActiveRadarrBlock::EditIndexerUrlInput
| ActiveRadarrBlock::EditIndexerApiKeyInput
| ActiveRadarrBlock::EditIndexerSeedRatioInput
| ActiveRadarrBlock::EditIndexerTagsInput => {
self.app.push_navigation_stack(selected_block.into());
self.app.should_ignore_quit_key = true;
}
ActiveRadarrBlock::EditIndexerToggleEnableRss => {
let indexer = self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap();
indexer.enable_rss = Some(!indexer.enable_rss.unwrap_or_default());
}
ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch => {
let indexer = self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap();
indexer.enable_automatic_search =
Some(!indexer.enable_automatic_search.unwrap_or_default());
}
ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch => {
let indexer = self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap();
indexer.enable_interactive_search =
Some(!indexer.enable_interactive_search.unwrap_or_default());
}
_ => (),
}
}
ActiveRadarrBlock::EditIndexerNameInput
| ActiveRadarrBlock::EditIndexerUrlInput
| ActiveRadarrBlock::EditIndexerApiKeyInput
| ActiveRadarrBlock::EditIndexerSeedRatioInput
| ActiveRadarrBlock::EditIndexerTagsInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::EditIndexerPrompt => {
self.app.pop_navigation_stack();
self.app.data.radarr_data.prompt_confirm = false;
self.app.data.radarr_data.edit_indexer_modal = None;
}
ActiveRadarrBlock::EditIndexerNameInput
| ActiveRadarrBlock::EditIndexerUrlInput
| ActiveRadarrBlock::EditIndexerApiKeyInput
| ActiveRadarrBlock::EditIndexerSeedRatioInput
| ActiveRadarrBlock::EditIndexerTagsInput => {
self.app.pop_navigation_stack();
self.app.should_ignore_quit_key = false;
}
_ => self.app.pop_navigation_stack(),
}
}
fn handle_char_key_event(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::EditIndexerNameInput => {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.name
);
}
ActiveRadarrBlock::EditIndexerUrlInput => {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.url
);
}
ActiveRadarrBlock::EditIndexerApiKeyInput => {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.api_key
);
}
ActiveRadarrBlock::EditIndexerSeedRatioInput => {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.seed_ratio
);
}
ActiveRadarrBlock::EditIndexerTagsInput => {
handle_text_box_keys!(
self,
self.key,
self
.app
.data
.radarr_data
.edit_indexer_modal
.as_mut()
.unwrap()
.tags
);
}
_ => (),
}
}
}
File diff suppressed because it is too large Load Diff
@@ -46,7 +46,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
let indexer_settings = self.app.data.radarr_data.indexer_settings.as_mut().unwrap();
match self.active_radarr_block {
ActiveRadarrBlock::IndexerSettingsPrompt => {
self.app.data.radarr_data.selected_block.previous()
self.app.data.radarr_data.selected_block.previous();
}
ActiveRadarrBlock::IndexerSettingsMinimumAgeInput => {
indexer_settings.minimum_age += 1;
@@ -166,7 +166,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
ActiveRadarrBlock::IndexerSettingsConfirmPrompt => {
let radarr_data = &mut self.app.data.radarr_data;
if radarr_data.prompt_confirm {
radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateIndexerSettings);
radarr_data.prompt_confirm_action = Some(RadarrEvent::EditAllIndexerSettings);
self.app.should_refresh = true;
} else {
radarr_data.indexer_settings = None;
@@ -456,7 +456,7 @@ mod tests {
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::UpdateIndexerSettings)
Some(RadarrEvent::EditAllIndexerSettings)
);
assert!(app.data.radarr_data.indexer_settings.is_some());
assert!(app.should_refresh);
@@ -7,10 +7,10 @@ mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::radarr_handlers::indexers::{IndexersHandler, TestAllIndexersHandler};
use crate::handlers::radarr_handlers::indexers::IndexersHandler;
use crate::handlers::KeyEventHandler;
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
ActiveRadarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
};
use crate::test_handler_delegation;
@@ -147,7 +147,14 @@ mod tests {
}
mod test_handle_submit {
use crate::models::radarr_models::{Indexer, IndexerField};
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::{
RadarrData, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
};
use bimap::BiMap;
use pretty_assertions::assert_eq;
use serde_json::{Number, Value};
use crate::network::radarr_network::RadarrEvent;
@@ -155,16 +162,85 @@ mod tests {
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test]
fn test_indexer_submit_aka_edit() {
#[rstest]
fn test_edit_indexer_submit(#[values(true, false)] torrent_protocol: bool) {
let mut app = App::default();
let protocol = if torrent_protocol {
"torrent".to_owned()
} else {
"usenet".to_owned()
};
let mut expected_edit_indexer_modal = EditIndexerModal {
name: "Test".into(),
enable_rss: Some(true),
enable_automatic_search: Some(true),
enable_interactive_search: Some(true),
url: "https://test.com".into(),
api_key: "1234".into(),
tags: "usenet, test".into(),
..EditIndexerModal::default()
};
let mut radarr_data = RadarrData {
tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]),
..RadarrData::default()
};
let mut fields = vec![
IndexerField {
name: Some("baseUrl".to_owned()),
value: Some(Value::String("https://test.com".to_owned())),
},
IndexerField {
name: Some("apiKey".to_owned()),
value: Some(Value::String("1234".to_owned())),
},
];
if torrent_protocol {
fields.push(IndexerField {
name: Some("seedCriteria.seedRatio".to_owned()),
value: Some(Value::from(1.2f64)),
});
expected_edit_indexer_modal.seed_ratio = "1.2".into();
}
let indexer = Indexer {
name: Some("Test".to_owned()),
enable_rss: true,
enable_automatic_search: true,
enable_interactive_search: true,
protocol,
tags: vec![Number::from(1), Number::from(2)],
fields: Some(fields),
..Indexer::default()
};
radarr_data.indexers.set_items(vec![indexer]);
app.data.radarr_data = radarr_data;
IndexersHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::EditIndexer.into()
&ActiveRadarrBlock::EditIndexerPrompt.into()
);
assert_eq!(
app.data.radarr_data.edit_indexer_modal,
Some((&app.data.radarr_data).into())
);
assert_eq!(
app.data.radarr_data.edit_indexer_modal,
Some(expected_edit_indexer_modal)
);
if torrent_protocol {
assert_eq!(
app.data.radarr_data.selected_block.blocks,
&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS
);
} else {
assert_eq!(
app.data.radarr_data.selected_block.blocks,
&EDIT_INDEXER_NZB_SELECTION_BLOCKS
);
}
}
#[test]
@@ -322,6 +398,29 @@ mod tests {
}
}
#[rstest]
fn test_delegates_edit_indexer_blocks_to_edit_indexer_handler(
#[values(
ActiveRadarrBlock::EditIndexerPrompt,
ActiveRadarrBlock::EditIndexerConfirmPrompt,
ActiveRadarrBlock::EditIndexerApiKeyInput,
ActiveRadarrBlock::EditIndexerNameInput,
ActiveRadarrBlock::EditIndexerSeedRatioInput,
ActiveRadarrBlock::EditIndexerToggleEnableRss,
ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveRadarrBlock::EditIndexerUrlInput,
ActiveRadarrBlock::EditIndexerTagsInput
)]
active_radarr_block: ActiveRadarrBlock,
) {
test_handler_delegation!(
IndexersHandler,
ActiveRadarrBlock::Indexers,
active_radarr_block
);
}
#[rstest]
fn test_delegates_indexer_settings_blocks_to_indexer_settings_handler(
#[values(
@@ -348,7 +447,7 @@ mod tests {
#[test]
fn test_delegates_test_all_indexers_block_to_test_all_indexers_handler() {
test_handler_delegation!(
TestAllIndexersHandler,
IndexersHandler,
ActiveRadarrBlock::Indexers,
ActiveRadarrBlock::TestAllIndexers
);
@@ -359,6 +458,8 @@ mod tests {
let mut indexers_blocks = Vec::new();
indexers_blocks.extend(INDEXERS_BLOCKS);
indexers_blocks.extend(INDEXER_SETTINGS_BLOCKS);
indexers_blocks.extend(EDIT_INDEXER_BLOCKS);
indexers_blocks.push(ActiveRadarrBlock::TestAllIndexers);
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if indexers_blocks.contains(&active_radarr_block) {
+28 -3
View File
@@ -2,15 +2,18 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
use crate::handlers::radarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler;
use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, INDEXERS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS,
ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
INDEXERS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS,
};
use crate::models::{BlockSelectionState, Scrollable};
use crate::network::radarr_network::RadarrEvent;
mod edit_indexer_handler;
mod edit_indexer_settings_handler;
mod test_all_indexers_handler;
@@ -28,6 +31,10 @@ pub(super) struct IndexersHandler<'a, 'b> {
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, 'b> {
fn handle(&mut self) {
match self.active_radarr_block {
_ if EditIndexerHandler::accepts(self.active_radarr_block) => {
EditIndexerHandler::with(self.key, self.app, self.active_radarr_block, self.context)
.handle()
}
_ if IndexerSettingsHandler::accepts(self.active_radarr_block) => {
IndexerSettingsHandler::with(self.key, self.app, self.active_radarr_block, self.context)
.handle()
@@ -41,7 +48,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
}
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool {
IndexerSettingsHandler::accepts(active_block) || INDEXERS_BLOCKS.contains(active_block)
EditIndexerHandler::accepts(active_block)
|| IndexerSettingsHandler::accepts(active_block)
|| TestAllIndexersHandler::accepts(active_block)
|| INDEXERS_BLOCKS.contains(active_block)
}
fn with(
@@ -115,7 +125,22 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
ActiveRadarrBlock::Indexers => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::EditIndexer.into());
.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into());
self.app.data.radarr_data.edit_indexer_modal = Some((&self.app.data.radarr_data).into());
let protocol = &self
.app
.data
.radarr_data
.indexers
.current_selection()
.protocol;
if protocol == "torrent" {
self.app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS);
} else {
self.app.data.radarr_data.selected_block =
BlockSelectionState::new(&EDIT_INDEXER_NZB_SELECTION_BLOCKS);
}
}
_ => (),
}
+1 -18
View File
@@ -164,30 +164,13 @@ pub struct Indexer {
pub priority: i64,
#[serde(deserialize_with = "super::from_i64")]
pub download_client_id: i64,
pub tags: Option<Vec<String>>,
pub tags: Vec<Number>,
}
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct IndexerField {
#[serde(deserialize_with = "super::from_i64")]
pub order: i64,
pub name: Option<String>,
pub label: Option<String>,
pub value: Option<Value>,
#[serde(rename(deserialize = "type"))]
pub field_type: Option<String>,
pub select_options: Option<Vec<IndexerSelectOption>>,
}
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct IndexerSelectOption {
#[serde(deserialize_with = "super::from_i64")]
pub value: i64,
pub name: Option<String>,
#[serde(deserialize_with = "super::from_i64")]
pub order: i64,
}
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
+92 -2
View File
@@ -1,6 +1,6 @@
use crate::models::radarr_models::{
Collection, Credit, MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release, ReleaseField,
RootFolder,
Collection, Credit, Indexer, MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release,
ReleaseField, RootFolder,
};
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
use crate::models::{HorizontallyScrollableText, ScrollableText, StatefulList, StatefulTable};
@@ -24,6 +24,96 @@ pub struct MovieDetailsModal {
pub sort_ascending: Option<bool>,
}
#[derive(Default, Debug, PartialEq, Eq)]
pub struct EditIndexerModal {
pub name: HorizontallyScrollableText,
pub enable_rss: Option<bool>,
pub enable_automatic_search: Option<bool>,
pub enable_interactive_search: Option<bool>,
pub url: HorizontallyScrollableText,
pub api_key: HorizontallyScrollableText,
pub seed_ratio: HorizontallyScrollableText,
pub tags: HorizontallyScrollableText,
}
impl From<&RadarrData<'_>> for EditIndexerModal {
fn from(radarr_data: &RadarrData<'_>) -> EditIndexerModal {
let mut edit_indexer_modal = EditIndexerModal::default();
let Indexer {
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
tags,
fields,
..
} = radarr_data.indexers.current_selection();
let seed_ratio_field_option = fields
.as_ref()
.unwrap()
.iter()
.find(|field| field.name.as_ref().unwrap() == "seedCriteria.seedRatio");
let seed_ratio_value_option = if let Some(seed_ratio_field) = seed_ratio_field_option {
seed_ratio_field.value.clone()
} else {
None
};
edit_indexer_modal.name = name.clone().unwrap().into();
edit_indexer_modal.enable_rss = Some(*enable_rss);
edit_indexer_modal.enable_automatic_search = Some(*enable_automatic_search);
edit_indexer_modal.enable_interactive_search = Some(*enable_interactive_search);
edit_indexer_modal.url = fields
.as_ref()
.unwrap()
.iter()
.find(|field| field.name.as_ref().unwrap() == "baseUrl")
.unwrap()
.value
.clone()
.unwrap()
.as_str()
.unwrap()
.into();
edit_indexer_modal.api_key = fields
.as_ref()
.unwrap()
.iter()
.find(|field| field.name.as_ref().unwrap() == "apiKey")
.unwrap()
.value
.clone()
.unwrap()
.as_str()
.unwrap()
.into();
if seed_ratio_value_option.is_some() {
edit_indexer_modal.seed_ratio = seed_ratio_value_option
.unwrap()
.as_f64()
.unwrap()
.to_string()
.into();
}
edit_indexer_modal.tags = tags
.iter()
.map(|tag_id| {
radarr_data
.tags_map
.get_by_left(&tag_id.as_i64().unwrap())
.unwrap()
.clone()
})
.collect::<Vec<String>>()
.join(", ")
.into();
edit_indexer_modal
}
}
#[derive(Default)]
pub struct EditMovieModal {
pub minimum_availability_list: StatefulList<MinimumAvailability>,
+99 -3
View File
@@ -1,8 +1,10 @@
#[cfg(test)]
mod test {
use crate::models::radarr_models::{Collection, MinimumAvailability, Monitor, Movie, RootFolder};
use crate::models::radarr_models::{
Collection, Indexer, IndexerField, MinimumAvailability, Monitor, Movie, RootFolder,
};
use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditMovieModal,
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal,
};
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
@@ -10,9 +12,103 @@ mod test {
use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use serde_json::Number;
use serde_json::{Number, Value};
use strum::IntoEnumIterator;
#[rstest]
fn test_edit_indexer_modal_from_radarr_data(#[values(true, false)] seed_ratio_present: bool) {
let mut radarr_data = RadarrData {
tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]),
..RadarrData::default()
};
let mut fields = vec![
IndexerField {
name: Some("baseUrl".to_owned()),
value: Some(Value::String("https://test.com".to_owned())),
},
IndexerField {
name: Some("apiKey".to_owned()),
value: Some(Value::String("1234".to_owned())),
},
];
if seed_ratio_present {
fields.push(IndexerField {
name: Some("seedCriteria.seedRatio".to_owned()),
value: Some(Value::from(1.2f64)),
});
}
let indexer = Indexer {
name: Some("Test".to_owned()),
enable_rss: true,
enable_automatic_search: true,
enable_interactive_search: true,
tags: vec![Number::from(1), Number::from(2)],
fields: Some(fields),
..Indexer::default()
};
radarr_data.indexers.set_items(vec![indexer]);
let edit_indexer_modal = EditIndexerModal::from(&radarr_data);
assert_str_eq!(edit_indexer_modal.name.text, "Test");
assert_eq!(edit_indexer_modal.enable_rss, Some(true));
assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true));
assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true));
assert_str_eq!(edit_indexer_modal.url.text, "https://test.com");
assert_str_eq!(edit_indexer_modal.api_key.text, "1234");
if seed_ratio_present {
assert_str_eq!(edit_indexer_modal.seed_ratio.text, "1.2");
} else {
assert!(edit_indexer_modal.seed_ratio.text.is_empty());
}
}
#[test]
fn test_edit_indexer_modal_from_radarr_data_seed_ratio_value_is_none() {
let mut radarr_data = RadarrData {
tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]),
..RadarrData::default()
};
let fields = vec![
IndexerField {
name: Some("baseUrl".to_owned()),
value: Some(Value::String("https://test.com".to_owned())),
},
IndexerField {
name: Some("apiKey".to_owned()),
value: Some(Value::String("1234".to_owned())),
},
IndexerField {
name: Some("seedCriteria.seedRatio".to_owned()),
value: None,
},
];
let indexer = Indexer {
name: Some("Test".to_owned()),
enable_rss: true,
enable_automatic_search: true,
enable_interactive_search: true,
tags: vec![Number::from(1), Number::from(2)],
fields: Some(fields),
..Indexer::default()
};
radarr_data.indexers.set_items(vec![indexer]);
let edit_indexer_modal = EditIndexerModal::from(&radarr_data);
assert_str_eq!(edit_indexer_modal.name.text, "Test");
assert_eq!(edit_indexer_modal.enable_rss, Some(true));
assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true));
assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true));
assert_str_eq!(edit_indexer_modal.url.text, "https://test.com");
assert_str_eq!(edit_indexer_modal.api_key.text, "1234");
assert!(edit_indexer_modal.seed_ratio.text.is_empty());
}
#[rstest]
fn test_edit_movie_modal_from_radarr_data(#[values(true, false)] test_filtered_movies: bool) {
let mut radarr_data = RadarrData {
+51 -5
View File
@@ -10,7 +10,8 @@ use crate::models::radarr_models::{
IndexerSettings, Movie, QueueEvent, RootFolder, Task,
};
use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditMovieModal, IndexerTestResultModalItem, MovieDetailsModal,
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
MovieDetailsModal,
};
use crate::models::{
BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, StatefulList,
@@ -55,6 +56,7 @@ pub struct RadarrData<'a> {
pub add_searched_movies: Option<StatefulTable<AddMovieSearchResult>>,
pub edit_movie_modal: Option<EditMovieModal>,
pub edit_collection_modal: Option<EditCollectionModal>,
pub edit_indexer_modal: Option<EditIndexerModal>,
pub edit_root_folder: Option<HorizontallyScrollableText>,
pub filtered_collections: Option<StatefulTable<Collection>>,
pub filtered_movies: Option<StatefulTable<Movie>>,
@@ -123,6 +125,7 @@ impl<'a> Default for RadarrData<'a> {
add_searched_movies: None,
edit_movie_modal: None,
edit_collection_modal: None,
edit_indexer_modal: None,
edit_root_folder: None,
filtered_collections: None,
filtered_movies: None,
@@ -252,7 +255,16 @@ pub enum ActiveRadarrBlock {
EditCollectionSelectQualityProfile,
EditCollectionToggleSearchOnAdd,
EditCollectionToggleMonitored,
EditIndexer,
EditIndexerPrompt,
EditIndexerConfirmPrompt,
EditIndexerApiKeyInput,
EditIndexerNameInput,
EditIndexerSeedRatioInput,
EditIndexerToggleEnableRss,
EditIndexerToggleEnableAutomaticSearch,
EditIndexerToggleEnableInteractiveSearch,
EditIndexerUrlInput,
EditIndexerTagsInput,
EditMoviePrompt,
EditMovieConfirmPrompt,
EditMoviePathInput,
@@ -318,12 +330,10 @@ pub static COLLECTIONS_BLOCKS: [ActiveRadarrBlock; 6] = [
ActiveRadarrBlock::FilterCollectionsError,
ActiveRadarrBlock::UpdateAllCollectionsPrompt,
];
pub static INDEXERS_BLOCKS: [ActiveRadarrBlock; 5] = [
pub static INDEXERS_BLOCKS: [ActiveRadarrBlock; 3] = [
ActiveRadarrBlock::AddIndexer,
ActiveRadarrBlock::EditIndexer,
ActiveRadarrBlock::DeleteIndexerPrompt,
ActiveRadarrBlock::Indexers,
ActiveRadarrBlock::TestAllIndexers,
];
pub static ROOT_FOLDERS_BLOCKS: [ActiveRadarrBlock; 3] = [
ActiveRadarrBlock::RootFolders,
@@ -416,6 +426,42 @@ pub static DELETE_MOVIE_SELECTION_BLOCKS: [ActiveRadarrBlock; 3] = [
ActiveRadarrBlock::DeleteMovieToggleAddListExclusion,
ActiveRadarrBlock::DeleteMovieConfirmPrompt,
];
pub static EDIT_INDEXER_BLOCKS: [ActiveRadarrBlock; 10] = [
ActiveRadarrBlock::EditIndexerPrompt,
ActiveRadarrBlock::EditIndexerConfirmPrompt,
ActiveRadarrBlock::EditIndexerApiKeyInput,
ActiveRadarrBlock::EditIndexerNameInput,
ActiveRadarrBlock::EditIndexerSeedRatioInput,
ActiveRadarrBlock::EditIndexerToggleEnableRss,
ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveRadarrBlock::EditIndexerUrlInput,
ActiveRadarrBlock::EditIndexerTagsInput,
];
pub static EDIT_INDEXER_TORRENT_SELECTION_BLOCKS: [ActiveRadarrBlock; 10] = [
ActiveRadarrBlock::EditIndexerNameInput,
ActiveRadarrBlock::EditIndexerToggleEnableRss,
ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveRadarrBlock::EditIndexerConfirmPrompt,
ActiveRadarrBlock::EditIndexerUrlInput,
ActiveRadarrBlock::EditIndexerApiKeyInput,
ActiveRadarrBlock::EditIndexerSeedRatioInput,
ActiveRadarrBlock::EditIndexerTagsInput,
ActiveRadarrBlock::EditIndexerConfirmPrompt,
];
pub static EDIT_INDEXER_NZB_SELECTION_BLOCKS: [ActiveRadarrBlock; 10] = [
ActiveRadarrBlock::EditIndexerNameInput,
ActiveRadarrBlock::EditIndexerToggleEnableRss,
ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch,
ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch,
ActiveRadarrBlock::EditIndexerConfirmPrompt,
ActiveRadarrBlock::EditIndexerUrlInput,
ActiveRadarrBlock::EditIndexerApiKeyInput,
ActiveRadarrBlock::EditIndexerTagsInput,
ActiveRadarrBlock::EditIndexerConfirmPrompt,
ActiveRadarrBlock::EditIndexerConfirmPrompt,
];
pub static INDEXER_SETTINGS_BLOCKS: [ActiveRadarrBlock; 10] = [
ActiveRadarrBlock::IndexerSettingsPrompt,
ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
@@ -98,6 +98,7 @@ mod tests {
assert!(radarr_data.edit_movie_modal.is_none());
assert!(radarr_data.edit_collection_modal.is_none());
assert!(radarr_data.edit_root_folder.is_none());
assert!(radarr_data.edit_indexer_modal.is_none());
assert!(radarr_data.filtered_collections.is_none());
assert!(radarr_data.filtered_movies.is_none());
assert!(radarr_data.indexer_settings.is_none());
@@ -270,9 +271,10 @@ mod tests {
ActiveRadarrBlock, ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, COLLECTIONS_BLOCKS,
COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS, DELETE_MOVIE_SELECTION_BLOCKS,
DOWNLOADS_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
EDIT_MOVIE_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
INDEXER_SETTINGS_SELECTION_BLOCKS, LIBRARY_BLOCKS, MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS,
SYSTEM_DETAILS_BLOCKS,
EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS,
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, EDIT_MOVIE_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS,
INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, LIBRARY_BLOCKS,
MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
};
#[test]
@@ -299,12 +301,10 @@ mod tests {
#[test]
fn test_indexers_blocks_contents() {
assert_eq!(INDEXERS_BLOCKS.len(), 5);
assert_eq!(INDEXERS_BLOCKS.len(), 3);
assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::AddIndexer));
assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::EditIndexer));
assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::DeleteIndexerPrompt));
assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::Indexers));
assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::TestAllIndexers));
}
#[test]
@@ -398,6 +398,25 @@ mod tests {
assert!(DELETE_MOVIE_BLOCKS.contains(&ActiveRadarrBlock::DeleteMovieToggleAddListExclusion));
}
#[test]
fn test_edit_indexer_blocks_contents() {
assert_eq!(EDIT_INDEXER_BLOCKS.len(), 10);
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerPrompt));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerConfirmPrompt));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerApiKeyInput));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerNameInput));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerSeedRatioInput));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerToggleEnableRss));
assert!(
EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch)
);
assert!(
EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch)
);
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerUrlInput));
assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerTagsInput));
}
#[test]
fn test_indexer_settings_blocks_contents() {
assert_eq!(INDEXER_SETTINGS_BLOCKS.len(), 10);
@@ -542,6 +561,101 @@ mod tests {
assert_eq!(delete_movie_block_iter.next(), None);
}
#[test]
fn test_edit_indexer_torrent_selection_blocks_ordering() {
let mut edit_indexer_torrent_selection_block_iter =
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.iter();
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerNameInput
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerToggleEnableRss
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerConfirmPrompt
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerUrlInput
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerApiKeyInput
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerSeedRatioInput
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerTagsInput
);
assert_eq!(
edit_indexer_torrent_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerConfirmPrompt
);
assert_eq!(edit_indexer_torrent_selection_block_iter.next(), None);
}
#[test]
fn test_edit_indexer_nzb_selection_blocks_ordering() {
let mut edit_indexer_nzb_selection_block_iter = EDIT_INDEXER_NZB_SELECTION_BLOCKS.iter();
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerNameInput
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerToggleEnableRss
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerConfirmPrompt
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerUrlInput
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerApiKeyInput
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerTagsInput
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerConfirmPrompt
);
assert_eq!(
edit_indexer_nzb_selection_block_iter.next().unwrap(),
&ActiveRadarrBlock::EditIndexerConfirmPrompt
);
assert_eq!(edit_indexer_nzb_selection_block_iter.next(), None);
}
#[test]
fn test_indexer_settings_selection_blocks_ordering() {
let mut indexer_settings_block_iter = INDEXER_SETTINGS_SELECTION_BLOCKS.iter();
+166 -38
View File
@@ -16,7 +16,8 @@ use crate::models::radarr_models::{
Update,
};
use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditMovieModal, IndexerTestResultModalItem, MovieDetailsModal,
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
MovieDetailsModal,
};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText, StatefulTable};
@@ -36,7 +37,9 @@ pub enum RadarrEvent {
DeleteMovie,
DeleteRootFolder,
DownloadRelease,
EditAllIndexerSettings,
EditCollection,
EditIndexer,
EditMovie,
GetCollections,
GetDownloads,
@@ -65,7 +68,6 @@ pub enum RadarrEvent {
UpdateAndScan,
UpdateCollections,
UpdateDownloads,
UpdateIndexerSettings,
}
impl RadarrEvent {
@@ -73,8 +75,10 @@ impl RadarrEvent {
match self {
RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection",
RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue",
RadarrEvent::GetIndexers | RadarrEvent::DeleteIndexer => "/indexer",
RadarrEvent::GetIndexerSettings | RadarrEvent::UpdateIndexerSettings => "/config/indexer",
RadarrEvent::GetIndexers | RadarrEvent::EditIndexer | RadarrEvent::DeleteIndexer => {
"/indexer"
}
RadarrEvent::GetIndexerSettings | RadarrEvent::EditAllIndexerSettings => "/config/indexer",
RadarrEvent::GetLogs => "/log",
RadarrEvent::AddMovie
| RadarrEvent::EditMovie
@@ -123,7 +127,9 @@ impl<'a, 'b> Network<'a, 'b> {
RadarrEvent::DeleteMovie => self.delete_movie().await,
RadarrEvent::DeleteRootFolder => self.delete_root_folder().await,
RadarrEvent::DownloadRelease => self.download_release().await,
RadarrEvent::EditAllIndexerSettings => self.edit_all_indexer_settings().await,
RadarrEvent::EditCollection => self.edit_collection().await,
RadarrEvent::EditIndexer => self.edit_indexer().await,
RadarrEvent::EditMovie => self.edit_movie().await,
RadarrEvent::GetCollections => self.get_collections().await,
RadarrEvent::GetDownloads => self.get_downloads().await,
@@ -152,7 +158,6 @@ impl<'a, 'b> Network<'a, 'b> {
RadarrEvent::UpdateAndScan => self.update_and_scan().await,
RadarrEvent::UpdateCollections => self.update_collections().await,
RadarrEvent::UpdateDownloads => self.update_downloads().await,
RadarrEvent::UpdateIndexerSettings => self.update_indexer_settings().await,
}
}
@@ -466,6 +471,37 @@ impl<'a, 'b> Network<'a, 'b> {
.await;
}
async fn edit_all_indexer_settings(&mut self) {
info!("Updating Radarr indexer settings");
let body = self
.app
.lock()
.await
.data
.radarr_data
.indexer_settings
.as_ref()
.unwrap()
.clone();
debug!("Indexer settings body: {body:?}");
let request_props = self
.radarr_request_props_from(
RadarrEvent::EditAllIndexerSettings.resource(),
RequestMethod::Put,
Some(body),
)
.await;
self
.handle_request::<IndexerSettings, Value>(request_props, |_, _| {})
.await;
self.app.lock().await.data.radarr_data.indexer_settings = None;
}
async fn edit_collection(&mut self) {
info!("Editing Radarr collection");
@@ -545,6 +581,129 @@ impl<'a, 'b> Network<'a, 'b> {
.await;
}
async fn edit_indexer(&mut self) {
let id = self
.app
.lock()
.await
.data
.radarr_data
.indexers
.current_selection()
.id;
info!("Updating Radarr indexer with ID: {id}");
info!("Fetching indexer details for indexer with ID: {id}");
let request_props = self
.radarr_request_props_from(
format!("{}/{id}", RadarrEvent::GetIndexers.resource()).as_str(),
RequestMethod::Get,
None::<()>,
)
.await;
let mut response = String::new();
self
.handle_request::<(), Value>(request_props, |detailed_indexer_body, _| {
response = detailed_indexer_body.to_string()
})
.await;
info!("Constructing edit indexer body");
let body = {
let tags = self
.app
.lock()
.await
.data
.radarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.tags
.text
.clone();
let tag_ids_vec = self.extract_and_add_tag_ids_vec(tags).await;
let mut app = self.app.lock().await;
let mut detailed_indexer_body: Value = serde_json::from_str(&response).unwrap();
let EditIndexerModal {
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
url,
api_key,
seed_ratio,
..
} = app.data.radarr_data.edit_indexer_modal.as_ref().unwrap();
*detailed_indexer_body.get_mut("name").unwrap() = json!(name.text.clone());
*detailed_indexer_body.get_mut("enableRss").unwrap() = json!(enable_rss.unwrap_or_default());
*detailed_indexer_body
.get_mut("enableAutomaticSearch")
.unwrap() = json!(enable_automatic_search.unwrap_or_default());
*detailed_indexer_body
.get_mut("enableInteractiveSearch")
.unwrap() = json!(enable_interactive_search.unwrap_or_default());
*detailed_indexer_body
.get_mut("fields")
.unwrap()
.as_array_mut()
.unwrap()
.iter_mut()
.find(|field| field["name"] == "baseUrl")
.unwrap()
.get_mut("value")
.unwrap() = json!(url.text.clone());
*detailed_indexer_body
.get_mut("fields")
.unwrap()
.as_array_mut()
.unwrap()
.iter_mut()
.find(|field| field["name"] == "apiKey")
.unwrap()
.get_mut("value")
.unwrap() = json!(api_key.text.clone());
*detailed_indexer_body.get_mut("tags").unwrap() = json!(tag_ids_vec);
let seed_ratio_field_option = detailed_indexer_body
.get_mut("fields")
.unwrap()
.as_array_mut()
.unwrap()
.iter_mut()
.find(|field| field["name"] == "seedCriteria.seedRatio");
if let Some(seed_ratio_field) = seed_ratio_field_option {
seed_ratio_field
.as_object_mut()
.unwrap()
.insert("value".to_string(), json!(seed_ratio.text.clone()));
}
app.data.radarr_data.edit_indexer_modal = None;
detailed_indexer_body
};
debug!("Edit indexer body: {body:?}");
let request_props = self
.radarr_request_props_from(
format!("{}/{id}", RadarrEvent::EditIndexer.resource()).as_str(),
RequestMethod::Put,
Some(body),
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await;
}
async fn edit_movie(&mut self) {
info!("Editing Radarr movie");
@@ -663,13 +822,13 @@ impl<'a, 'b> Network<'a, 'b> {
.handle_request::<(), Vec<Credit>>(request_props, |credit_vec, mut app| {
let cast_vec: Vec<Credit> = credit_vec
.iter()
.filter(|&credit| credit.credit_type == CreditType::Cast)
.cloned()
.filter(|credit| credit.credit_type == CreditType::Cast)
.collect();
let crew_vec: Vec<Credit> = credit_vec
.iter()
.filter(|&credit| credit.credit_type == CreditType::Crew)
.cloned()
.filter(|credit| credit.credit_type == CreditType::Crew)
.collect();
if app.data.radarr_data.movie_details_modal.is_none() {
@@ -1500,37 +1659,6 @@ impl<'a, 'b> Network<'a, 'b> {
.await;
}
async fn update_indexer_settings(&mut self) {
info!("Updating Radarr indexer settings");
let body = self
.app
.lock()
.await
.data
.radarr_data
.indexer_settings
.as_ref()
.unwrap()
.clone();
debug!("Indexer settings body: {body:?}");
let request_props = self
.radarr_request_props_from(
RadarrEvent::UpdateIndexerSettings.resource(),
RequestMethod::Put,
Some(body),
)
.await;
self
.handle_request::<IndexerSettings, Value>(request_props, |_, _| {})
.await;
self.app.lock().await.data.radarr_data.indexer_settings = None;
}
async fn radarr_request_props_from<T: Serialize + Debug>(
&self,
resource: &str,
+409 -140
View File
@@ -13,8 +13,8 @@ mod test {
use tokio_util::sync::CancellationToken;
use crate::models::radarr_models::{
CollectionMovie, IndexerField, IndexerSelectOption, Language, MediaInfo, MinimumAvailability,
Monitor, MovieFile, Quality, QualityWrapper, Rating, RatingsList,
CollectionMovie, IndexerField, Language, MediaInfo, MinimumAvailability, Monitor, MovieFile,
Quality, QualityWrapper, Rating, RatingsList,
};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::{HorizontallyScrollableText, StatefulTable};
@@ -136,7 +136,7 @@ mod test {
#[rstest]
fn test_resource_indexer_settings(
#[values(RadarrEvent::GetIndexerSettings, RadarrEvent::UpdateIndexerSettings)]
#[values(RadarrEvent::GetIndexerSettings, RadarrEvent::EditAllIndexerSettings)]
event: RadarrEvent,
) {
assert_str_eq!(event.resource(), "/config/indexer");
@@ -782,45 +782,6 @@ mod test {
async_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_update_indexer_settings_event() {
let indexer_settings_json = json!({
"minimumAge": 0,
"maximumSize": 0,
"retention": 0,
"rssSyncInterval": 60,
"preferIndexerFlags": false,
"availabilityDelay": 0,
"allowHardcodedSubs": true,
"whitelistedHardcodedSubs": "",
"id": 1
});
let (async_server, app_arc, _server) = mock_radarr_api(
RequestMethod::Put,
Some(indexer_settings_json),
None,
None,
RadarrEvent::UpdateIndexerSettings.resource(),
)
.await;
app_arc.lock().await.data.radarr_data.indexer_settings = Some(indexer_settings());
let mut network = Network::new(&app_arc, CancellationToken::new());
network
.handle_radarr_event(RadarrEvent::UpdateIndexerSettings)
.await;
async_server.assert_async().await;
assert!(app_arc
.lock()
.await
.data
.radarr_data
.indexer_settings
.is_none());
}
#[tokio::test]
async fn test_handle_update_collections_event() {
let (async_server, app_arc, _server) = mock_radarr_api(
@@ -1211,37 +1172,22 @@ mod test {
"name": "Test Indexer",
"fields": [
{
"order": 0,
"name": "valueIsString",
"label": "Value Is String",
"value": "hello",
"type": "textbox",
"name": "baseUrl",
"value": "https://test.com",
},
{
"order": 1,
"name": "emptyValueWithSelectOptions",
"label": "Empty Value With Select Options",
"type": "select",
"selectOptions": [
{
"value": -2,
"name": "Original",
"order": 0,
}
]
"name": "apiKey",
"value": "",
},
{
"order": 2,
"name": "valueIsAnArray",
"label": "Value is an array",
"value": [1, 2],
"type": "select",
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"implementationName": "Torznab",
"implementation": "Torznab",
"configContract": "TorznabSettings",
"tags": ["test_tag"],
"tags": [1],
"id": 1
}]);
let (async_server, app_arc, _server) = mock_radarr_api(
@@ -2083,66 +2029,42 @@ mod test {
}
#[tokio::test]
async fn test_handle_edit_movie_event() {
let mut expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap();
*expected_body.get_mut("monitored").unwrap() = json!(false);
*expected_body.get_mut("minimumAvailability").unwrap() = json!("announced");
*expected_body.get_mut("qualityProfileId").unwrap() = json!(1111);
*expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path");
*expected_body.get_mut("tags").unwrap() = json!([1, 2]);
let resource = format!("{}/1", RadarrEvent::GetMovieDetails.resource());
let (async_details_server, app_arc, mut server) = mock_radarr_api(
RequestMethod::Get,
async fn test_handle_edit_all_indexer_settings_event() {
let indexer_settings_json = json!({
"minimumAge": 0,
"maximumSize": 0,
"retention": 0,
"rssSyncInterval": 60,
"preferIndexerFlags": false,
"availabilityDelay": 0,
"allowHardcodedSubs": true,
"whitelistedHardcodedSubs": "",
"id": 1
});
let (async_server, app_arc, _server) = mock_radarr_api(
RequestMethod::Put,
Some(indexer_settings_json),
None,
Some(serde_json::from_str(MOVIE_JSON).unwrap()),
None,
&resource,
RadarrEvent::EditAllIndexerSettings.resource(),
)
.await;
let async_edit_server = server
.mock(
"PUT",
format!("/api/v3{}/1", RadarrEvent::EditMovie.resource()).as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_body))
.create_async()
.await;
{
let mut app = app_arc.lock().await;
app.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
let mut edit_movie = EditMovieModal {
tags: "usenet, testing".to_owned().into(),
path: "/nfs/Test Path".to_owned().into(),
monitored: Some(false),
..EditMovieModal::default()
};
edit_movie
.quality_profile_list
.set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]);
edit_movie
.minimum_availability_list
.set_items(Vec::from_iter(MinimumAvailability::iter()));
app.data.radarr_data.edit_movie_modal = Some(edit_movie);
app.data.radarr_data.movies.set_items(vec![Movie {
monitored: false,
..movie()
}]);
app.data.radarr_data.quality_profile_map =
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
}
app_arc.lock().await.data.radarr_data.indexer_settings = Some(indexer_settings());
let mut network = Network::new(&app_arc, CancellationToken::new());
network.handle_radarr_event(RadarrEvent::EditMovie).await;
network
.handle_radarr_event(RadarrEvent::EditAllIndexerSettings)
.await;
async_details_server.assert_async().await;
async_edit_server.assert_async().await;
let app = app_arc.lock().await;
assert!(app.data.radarr_data.edit_movie_modal.is_none());
async_server.assert_async().await;
assert!(app_arc
.lock()
.await
.data
.radarr_data
.indexer_settings
.is_none());
}
#[tokio::test]
@@ -2240,6 +2162,369 @@ mod test {
assert!(app.data.radarr_data.edit_collection_modal.is_none());
}
#[tokio::test]
async fn test_handle_edit_indexer_event() {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [1],
"id": 1
});
let expected_indexer_edit_body_json = json!({
"enableRss": false,
"enableAutomaticSearch": false,
"enableInteractiveSearch": false,
"name": "Test Update",
"fields": [
{
"name": "baseUrl",
"value": "https://localhost:9696/1/",
},
{
"name": "apiKey",
"value": "test1234",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.3",
},
],
"tags": [1, 2],
"id": 1
});
let resource = format!("{}/1", RadarrEvent::GetIndexers.resource());
let (async_details_server, app_arc, mut server) = mock_radarr_api(
RequestMethod::Get,
None,
Some(indexer_details_json),
None,
&resource,
)
.await;
let async_edit_server = server
.mock(
"PUT",
format!("/api/v3{}/1", RadarrEvent::EditIndexer.resource()).as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
{
let mut app = app_arc.lock().await;
app.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
let edit_indexer_modal = EditIndexerModal {
name: "Test Update".into(),
enable_rss: Some(false),
enable_automatic_search: Some(false),
enable_interactive_search: Some(false),
url: "https://localhost:9696/1/".into(),
api_key: "test1234".into(),
seed_ratio: "1.3".into(),
tags: "usenet, testing".into(),
};
app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal);
app.data.radarr_data.indexers.set_items(vec![indexer()]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
network.handle_radarr_event(RadarrEvent::EditIndexer).await;
async_details_server.assert_async().await;
async_edit_server.assert_async().await;
let app = app_arc.lock().await;
assert!(app.data.radarr_data.edit_indexer_modal.is_none());
}
#[tokio::test]
async fn test_handle_edit_indexer_event_does_not_add_seed_ratio_when_seed_ratio_field_is_none_in_details(
) {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
],
"tags": [1],
"id": 1
});
let expected_indexer_edit_body_json = json!({
"enableRss": false,
"enableAutomaticSearch": false,
"enableInteractiveSearch": false,
"name": "Test Update",
"fields": [
{
"name": "baseUrl",
"value": "https://localhost:9696/1/",
},
{
"name": "apiKey",
"value": "test1234",
},
],
"tags": [1, 2],
"id": 1
});
let resource = format!("{}/1", RadarrEvent::GetIndexers.resource());
let (async_details_server, app_arc, mut server) = mock_radarr_api(
RequestMethod::Get,
None,
Some(indexer_details_json),
None,
&resource,
)
.await;
let async_edit_server = server
.mock(
"PUT",
format!("/api/v3{}/1", RadarrEvent::EditIndexer.resource()).as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
{
let mut app = app_arc.lock().await;
app.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
let edit_indexer_modal = EditIndexerModal {
name: "Test Update".into(),
enable_rss: Some(false),
enable_automatic_search: Some(false),
enable_interactive_search: Some(false),
url: "https://localhost:9696/1/".into(),
api_key: "test1234".into(),
seed_ratio: "1.3".into(),
tags: "usenet, testing".into(),
};
app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal);
let mut indexer = indexer();
indexer.fields = Some(
indexer
.fields
.unwrap()
.into_iter()
.filter(|field| field.name != Some("seedCriteria.seedRatio".to_string()))
.collect(),
);
app.data.radarr_data.indexers.set_items(vec![indexer]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
network.handle_radarr_event(RadarrEvent::EditIndexer).await;
async_details_server.assert_async().await;
async_edit_server.assert_async().await;
let app = app_arc.lock().await;
assert!(app.data.radarr_data.edit_indexer_modal.is_none());
}
#[tokio::test]
async fn test_handle_edit_indexer_event_populates_the_seed_ratio_value_when_seed_ratio_field_is_present_in_details(
) {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
},
],
"tags": [1],
"id": 1
});
let expected_indexer_edit_body_json = json!({
"enableRss": false,
"enableAutomaticSearch": false,
"enableInteractiveSearch": false,
"name": "Test Update",
"fields": [
{
"name": "baseUrl",
"value": "https://localhost:9696/1/",
},
{
"name": "apiKey",
"value": "test1234",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.3",
},
],
"tags": [1, 2],
"id": 1
});
let resource = format!("{}/1", RadarrEvent::GetIndexers.resource());
let (async_details_server, app_arc, mut server) = mock_radarr_api(
RequestMethod::Get,
None,
Some(indexer_details_json),
None,
&resource,
)
.await;
let async_edit_server = server
.mock(
"PUT",
format!("/api/v3{}/1", RadarrEvent::EditIndexer.resource()).as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
{
let mut app = app_arc.lock().await;
app.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
let edit_indexer_modal = EditIndexerModal {
name: "Test Update".into(),
enable_rss: Some(false),
enable_automatic_search: Some(false),
enable_interactive_search: Some(false),
url: "https://localhost:9696/1/".into(),
api_key: "test1234".into(),
seed_ratio: "1.3".into(),
tags: "usenet, testing".into(),
};
app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal);
let mut indexer = indexer();
indexer.fields = Some(
indexer
.fields
.unwrap()
.into_iter()
.map(|mut field| {
if field.name == Some("seedCriteria.seedRatio".to_string()) {
field.value = None;
field
} else {
field
}
})
.collect(),
);
app.data.radarr_data.indexers.set_items(vec![indexer]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
network.handle_radarr_event(RadarrEvent::EditIndexer).await;
async_details_server.assert_async().await;
async_edit_server.assert_async().await;
let app = app_arc.lock().await;
assert!(app.data.radarr_data.edit_indexer_modal.is_none());
}
#[tokio::test]
async fn test_handle_edit_movie_event() {
let mut expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap();
*expected_body.get_mut("monitored").unwrap() = json!(false);
*expected_body.get_mut("minimumAvailability").unwrap() = json!("announced");
*expected_body.get_mut("qualityProfileId").unwrap() = json!(1111);
*expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path");
*expected_body.get_mut("tags").unwrap() = json!([1, 2]);
let resource = format!("{}/1", RadarrEvent::GetMovieDetails.resource());
let (async_details_server, app_arc, mut server) = mock_radarr_api(
RequestMethod::Get,
None,
Some(serde_json::from_str(MOVIE_JSON).unwrap()),
None,
&resource,
)
.await;
let async_edit_server = server
.mock(
"PUT",
format!("/api/v3{}/1", RadarrEvent::EditMovie.resource()).as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_body))
.create_async()
.await;
{
let mut app = app_arc.lock().await;
app.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
let mut edit_movie = EditMovieModal {
tags: "usenet, testing".to_owned().into(),
path: "/nfs/Test Path".to_owned().into(),
monitored: Some(false),
..EditMovieModal::default()
};
edit_movie
.quality_profile_list
.set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]);
edit_movie
.minimum_availability_list
.set_items(Vec::from_iter(MinimumAvailability::iter()));
app.data.radarr_data.edit_movie_modal = Some(edit_movie);
app.data.radarr_data.movies.set_items(vec![Movie {
monitored: false,
..movie()
}]);
app.data.radarr_data.quality_profile_map =
BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
network.handle_radarr_event(RadarrEvent::EditMovie).await;
async_details_server.assert_async().await;
async_edit_server.assert_async().await;
let app = app_arc.lock().await;
assert!(app.data.radarr_data.edit_movie_modal.is_none());
}
#[tokio::test]
async fn test_handle_download_release_event() {
let (async_server, app_arc, _server) = mock_radarr_api(
@@ -2786,36 +3071,20 @@ mod test {
implementation_name: Some("Torznab".to_owned()),
implementation: Some("Torznab".to_owned()),
config_contract: Some("TorznabSettings".to_owned()),
tags: Some(vec!["test_tag".to_owned()]),
tags: vec![Number::from(1)],
id: 1,
fields: Some(vec![
IndexerField {
order: 0,
name: Some("valueIsString".to_owned()),
label: Some("Value Is String".to_owned()),
value: Some(json!("hello")),
field_type: Some("textbox".to_owned()),
select_options: None,
name: Some("baseUrl".to_owned()),
value: Some(json!("https://test.com")),
},
IndexerField {
order: 1,
name: Some("emptyValueWithSelectOptions".to_owned()),
label: Some("Empty Value With Select Options".to_owned()),
value: None,
field_type: Some("select".to_owned()),
select_options: Some(vec![IndexerSelectOption {
value: -2,
name: Some("Original".to_owned()),
order: 0,
}]),
name: Some("apiKey".to_owned()),
value: Some(json!("")),
},
IndexerField {
order: 2,
name: Some("valueIsAnArray".to_owned()),
label: Some("Value is an array".to_owned()),
value: Some(json!([1, 2])),
field_type: Some("select".to_owned()),
select_options: None,
name: Some("seedCriteria.seedRatio".to_owned()),
value: Some(json!("1.2")),
},
]),
}
+59 -28
View File
@@ -676,15 +676,26 @@ fn draw_help_and_get_content_rect(f: &mut Frame<'_>, area: Rect, help: Option<St
}
}
pub fn draw_text_box(
f: &mut Frame<'_>,
text_box_area: Rect,
block_title: Option<&str>,
block_content: &str,
offset: usize,
should_show_cursor: bool,
is_selected: bool,
) {
pub struct TextBoxProps<'a> {
pub text_box_area: Rect,
pub block_title: Option<&'a str>,
pub block_content: &'a str,
pub offset: usize,
pub should_show_cursor: bool,
pub is_selected: bool,
pub cursor_after_string: bool,
}
pub fn draw_text_box(f: &mut Frame<'_>, text_box_props: TextBoxProps<'_>) {
let TextBoxProps {
text_box_area,
block_title,
block_content,
offset,
should_show_cursor,
is_selected,
cursor_after_string,
} = text_box_props;
let (block, style) = if let Some(title) = block_title {
(title_block_centered(title), style_default())
} else {
@@ -703,19 +714,33 @@ pub fn draw_text_box(
f.render_widget(paragraph, text_box_area);
if should_show_cursor {
show_cursor(f, text_box_area, offset, block_content);
show_cursor(f, text_box_area, offset, block_content, cursor_after_string);
}
}
pub struct LabeledTextBoxProps<'a> {
pub area: Rect,
pub label: &'a str,
pub text: &'a str,
pub offset: usize,
pub is_selected: bool,
pub should_show_cursor: bool,
pub cursor_after_string: bool,
}
pub fn draw_text_box_with_label(
f: &mut Frame<'_>,
area: Rect,
label: &str,
text: &str,
offset: usize,
is_selected: bool,
should_show_cursor: bool,
labeled_text_box_props: LabeledTextBoxProps<'_>,
) {
let LabeledTextBoxProps {
area,
label,
text,
offset,
is_selected,
should_show_cursor,
cursor_after_string,
} = labeled_text_box_props;
let horizontal_chunks = horizontal_chunks(
vec![
Constraint::Percentage(48),
@@ -734,12 +759,15 @@ pub fn draw_text_box_with_label(
draw_text_box(
f,
horizontal_chunks[1],
None,
text,
offset,
should_show_cursor,
is_selected,
TextBoxProps {
text_box_area: horizontal_chunks[1],
block_title: None,
block_content: text,
offset,
should_show_cursor,
is_selected,
cursor_after_string,
},
);
}
@@ -761,12 +789,15 @@ pub fn draw_input_box_popup(
draw_text_box(
f,
chunks[0],
Some(box_title),
&box_content.text,
*box_content.offset.borrow(),
true,
false,
TextBoxProps {
text_box_area: chunks[0],
block_title: Some(box_title),
block_content: &box_content.text,
offset: *box_content.offset.borrow(),
should_show_cursor: true,
is_selected: false,
cursor_after_string: true,
},
);
let help = Paragraph::new("<esc> cancel")
@@ -16,7 +16,7 @@ use crate::ui::utils::{
use crate::ui::{
draw_button, draw_checkbox_with_label, draw_drop_down_menu_button, draw_drop_down_popup,
draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over, draw_popup,
draw_selectable_list, draw_text_box_with_label, DrawUi,
draw_selectable_list, draw_text_box_with_label, DrawUi, LabeledTextBoxProps,
};
#[cfg(test)]
@@ -191,12 +191,16 @@ fn draw_edit_collection_confirmation_prompt(
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
draw_text_box_with_label(
f,
chunks[4],
"Root Folder",
&path.text,
*path.offset.borrow(),
selected_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput,
active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput,
LabeledTextBoxProps {
area: chunks[4],
label: "Root Folder",
text: &path.text,
offset: *path.offset.borrow(),
is_selected: selected_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput,
should_show_cursor: active_radarr_block
== ActiveRadarrBlock::EditCollectionRootFolderPathInput,
cursor_after_string: true,
},
);
}
@@ -0,0 +1,214 @@
use crate::app::App;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use crate::models::Route;
use crate::ui::radarr_ui::indexers::draw_indexers;
use crate::ui::utils::{
horizontal_chunks, horizontal_chunks_with_margin, title_block_centered, vertical_chunks,
vertical_chunks_with_margin,
};
use crate::ui::{
draw_button, draw_checkbox_with_label, draw_popup_over, draw_text_box_with_label, loading,
DrawUi, LabeledTextBoxProps,
};
use ratatui::layout::{Constraint, Rect};
use ratatui::Frame;
use std::iter;
#[cfg(test)]
#[path = "edit_indexer_ui_tests.rs"]
mod edit_indexer_ui_tests;
pub(super) struct EditIndexerUi;
impl DrawUi for EditIndexerUi {
fn accepts(route: Route) -> bool {
if let Route::Radarr(active_radarr_block, _) = route {
return EDIT_INDEXER_BLOCKS.contains(&active_radarr_block);
}
false
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, content_rect: Rect) {
draw_popup_over(
f,
app,
content_rect,
draw_indexers,
draw_edit_indexer_prompt,
70,
45,
);
}
}
fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: Rect) {
let block = title_block_centered("Edit Indexer");
let yes_no_value = app.data.radarr_data.prompt_confirm;
let selected_block = app.data.radarr_data.selected_block.get_active_block();
let highlight_yes_no = selected_block == &ActiveRadarrBlock::EditIndexerConfirmPrompt;
let edit_indexer_modal_option = &app.data.radarr_data.edit_indexer_modal;
let protocol = &app.data.radarr_data.indexers.current_selection().protocol;
if edit_indexer_modal_option.is_some() {
let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap();
f.render_widget(block, prompt_area);
let chunks = vertical_chunks_with_margin(
vec![Constraint::Min(0), Constraint::Length(3)],
prompt_area,
1,
);
let split_chunks = horizontal_chunks_with_margin(
vec![Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
chunks[0],
1,
);
let left_chunks = vertical_chunks(
vec![
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
],
split_chunks[0],
);
let right_chunks = vertical_chunks(
vec![
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
],
split_chunks[1],
);
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
draw_text_box_with_label(
f,
LabeledTextBoxProps {
area: left_chunks[0],
label: "Name",
text: &edit_indexer_modal.name.text,
offset: *edit_indexer_modal.name.offset.borrow(),
is_selected: selected_block == &ActiveRadarrBlock::EditIndexerNameInput,
should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditIndexerNameInput,
cursor_after_string: true,
},
);
draw_text_box_with_label(
f,
LabeledTextBoxProps {
area: right_chunks[0],
label: "URL",
text: &edit_indexer_modal.url.text,
offset: *edit_indexer_modal.url.offset.borrow(),
is_selected: selected_block == &ActiveRadarrBlock::EditIndexerUrlInput,
should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditIndexerUrlInput,
cursor_after_string: true,
},
);
draw_text_box_with_label(
f,
LabeledTextBoxProps {
area: right_chunks[1],
label: "API Key",
text: &edit_indexer_modal.api_key.text,
offset: *edit_indexer_modal.api_key.offset.borrow(),
is_selected: selected_block == &ActiveRadarrBlock::EditIndexerApiKeyInput,
should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditIndexerApiKeyInput,
cursor_after_string: true,
},
);
if protocol == "torrent" {
draw_text_box_with_label(
f,
LabeledTextBoxProps {
area: right_chunks[2],
label: "Seed Ratio",
text: &edit_indexer_modal.seed_ratio.text,
offset: *edit_indexer_modal.seed_ratio.offset.borrow(),
is_selected: selected_block == &ActiveRadarrBlock::EditIndexerSeedRatioInput,
should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditIndexerSeedRatioInput,
cursor_after_string: true,
},
);
draw_text_box_with_label(
f,
LabeledTextBoxProps {
area: right_chunks[3],
label: "Tags",
text: &edit_indexer_modal.tags.text,
offset: *edit_indexer_modal.tags.offset.borrow(),
is_selected: selected_block == &ActiveRadarrBlock::EditIndexerTagsInput,
should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditIndexerTagsInput,
cursor_after_string: true,
},
);
} else {
draw_text_box_with_label(
f,
LabeledTextBoxProps {
area: right_chunks[2],
label: "Tags",
text: &edit_indexer_modal.tags.text,
offset: *edit_indexer_modal.tags.offset.borrow(),
is_selected: selected_block == &ActiveRadarrBlock::EditIndexerTagsInput,
should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditIndexerTagsInput,
cursor_after_string: true,
},
);
}
draw_checkbox_with_label(
f,
left_chunks[1],
"Enable RSS",
edit_indexer_modal.enable_rss.unwrap_or_default(),
selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableRss,
);
draw_checkbox_with_label(
f,
left_chunks[2],
"Enable Automatic Search",
edit_indexer_modal
.enable_automatic_search
.unwrap_or_default(),
selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch,
);
draw_checkbox_with_label(
f,
left_chunks[3],
"Enable Interactive Search",
edit_indexer_modal
.enable_interactive_search
.unwrap_or_default(),
selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch,
);
let button_chunks = horizontal_chunks(
iter::repeat(Constraint::Ratio(1, 4)).take(4).collect(),
chunks[1],
);
draw_button(
f,
button_chunks[1],
"Save",
yes_no_value && highlight_yes_no,
);
draw_button(
f,
button_chunks[2],
"Cancel",
!yes_no_value && highlight_yes_no,
);
}
} else {
loading(f, block, prompt_area, app.is_loading);
}
}
@@ -0,0 +1,18 @@
#[cfg(test)]
mod tests {
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use crate::ui::radarr_ui::indexers::edit_indexer_ui::EditIndexerUi;
use crate::ui::DrawUi;
use strum::IntoEnumIterator;
#[test]
fn test_edit_indexer_ui_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if EDIT_INDEXER_BLOCKS.contains(&active_radarr_block) {
assert!(EditIndexerUi::accepts(active_radarr_block.into()));
} else {
assert!(!EditIndexerUi::accepts(active_radarr_block.into()));
}
});
}
}
@@ -13,7 +13,8 @@ use crate::ui::utils::{
vertical_chunks_with_margin,
};
use crate::ui::{
draw_button, draw_checkbox_with_label, draw_popup_over, draw_text_box_with_label, loading, DrawUi,
draw_button, draw_checkbox_with_label, draw_popup_over, draw_text_box_with_label, loading,
DrawUi, LabeledTextBoxProps,
};
#[cfg(test)]
@@ -91,57 +92,82 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, promp
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
draw_text_box_with_label(
f,
left_chunks[0],
"Minimum Age (minutes) ▴▾",
&indexer_settings.minimum_age.to_string(),
0,
selected_block == &ActiveRadarrBlock::IndexerSettingsMinimumAgeInput,
active_radarr_block == ActiveRadarrBlock::IndexerSettingsMinimumAgeInput,
LabeledTextBoxProps {
area: left_chunks[0],
label: "Minimum Age (minutes) ▴▾",
text: &indexer_settings.minimum_age.to_string(),
offset: 0,
is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsMinimumAgeInput,
should_show_cursor: active_radarr_block
== ActiveRadarrBlock::IndexerSettingsMinimumAgeInput,
cursor_after_string: false,
},
);
draw_text_box_with_label(
f,
left_chunks[1],
"Retention (days) ▴▾",
&indexer_settings.retention.to_string(),
0,
selected_block == &ActiveRadarrBlock::IndexerSettingsRetentionInput,
active_radarr_block == ActiveRadarrBlock::IndexerSettingsRetentionInput,
LabeledTextBoxProps {
area: left_chunks[1],
label: "Retention (days) ▴▾",
text: &indexer_settings.retention.to_string(),
offset: 0,
is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsRetentionInput,
should_show_cursor: active_radarr_block
== ActiveRadarrBlock::IndexerSettingsRetentionInput,
cursor_after_string: false,
},
);
draw_text_box_with_label(
f,
left_chunks[2],
"Maximum Size (MB) ▴▾",
&indexer_settings.maximum_size.to_string(),
0,
selected_block == &ActiveRadarrBlock::IndexerSettingsMaximumSizeInput,
active_radarr_block == ActiveRadarrBlock::IndexerSettingsMaximumSizeInput,
LabeledTextBoxProps {
area: left_chunks[2],
label: "Maximum Size (MB) ▴▾",
text: &indexer_settings.maximum_size.to_string(),
offset: 0,
is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsMaximumSizeInput,
should_show_cursor: active_radarr_block
== ActiveRadarrBlock::IndexerSettingsMaximumSizeInput,
cursor_after_string: false,
},
);
draw_text_box_with_label(
f,
right_chunks[0],
"Availability Delay (days) ▴▾",
&indexer_settings.availability_delay.to_string(),
0,
selected_block == &ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
active_radarr_block == ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
LabeledTextBoxProps {
area: right_chunks[0],
label: "Availability Delay (days) ▴▾",
text: &indexer_settings.availability_delay.to_string(),
offset: 0,
is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
should_show_cursor: active_radarr_block
== ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
cursor_after_string: false,
},
);
draw_text_box_with_label(
f,
right_chunks[1],
"RSS Sync Interval (minutes) ▴▾",
&indexer_settings.rss_sync_interval.to_string(),
0,
selected_block == &ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput,
active_radarr_block == ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput,
LabeledTextBoxProps {
area: right_chunks[1],
label: "RSS Sync Interval (minutes) ▴▾",
text: &indexer_settings.rss_sync_interval.to_string(),
offset: 0,
is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput,
should_show_cursor: active_radarr_block
== ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput,
cursor_after_string: false,
},
);
draw_text_box_with_label(
f,
right_chunks[2],
"Whitelisted Subtitle Tags",
&indexer_settings.whitelisted_hardcoded_subs.text,
*indexer_settings.whitelisted_hardcoded_subs.offset.borrow(),
selected_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
active_radarr_block == ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
LabeledTextBoxProps {
area: right_chunks[2],
label: "Whitelisted Subtitle Tags",
text: &indexer_settings.whitelisted_hardcoded_subs.text,
offset: *indexer_settings.whitelisted_hardcoded_subs.offset.borrow(),
is_selected: selected_block
== &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
should_show_cursor: active_radarr_block
== ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput,
cursor_after_string: true,
},
);
}
@@ -3,7 +3,7 @@ mod tests {
use strum::IntoEnumIterator;
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
ActiveRadarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS,
};
use crate::ui::radarr_ui::indexers::IndexersUi;
use crate::ui::DrawUi;
@@ -13,6 +13,8 @@ mod tests {
let mut indexers_blocks = Vec::new();
indexers_blocks.extend(INDEXERS_BLOCKS);
indexers_blocks.extend(INDEXER_SETTINGS_BLOCKS);
indexers_blocks.extend(EDIT_INDEXER_BLOCKS);
indexers_blocks.push(ActiveRadarrBlock::TestAllIndexers);
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if indexers_blocks.contains(&active_radarr_block) {
+30 -7
View File
@@ -7,24 +7,29 @@ use crate::app::App;
use crate::models::radarr_models::Indexer;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, INDEXERS_BLOCKS};
use crate::models::Route;
use crate::ui::radarr_ui::indexers::edit_indexer_ui::EditIndexerUi;
use crate::ui::radarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi;
use crate::ui::radarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi;
use crate::ui::utils::{layout_block_top_border, style_failure, style_primary, style_success};
use crate::ui::{draw_prompt_box, draw_prompt_popup_over, draw_table, DrawUi, TableProps};
mod edit_indexer_ui;
mod indexer_settings_ui;
mod test_all_indexers_ui;
#[cfg(test)]
#[path = "indexers_ui_tests.rs"]
mod indexers_ui_tests;
mod test_all_indexers_ui;
pub(super) struct IndexersUi;
impl DrawUi for IndexersUi {
fn accepts(route: Route) -> bool {
if let Route::Radarr(active_radarr_block, _) = route {
return IndexerSettingsUi::accepts(route) || INDEXERS_BLOCKS.contains(&active_radarr_block);
return EditIndexerUi::accepts(route)
|| IndexerSettingsUi::accepts(route)
|| TestAllIndexersUi::accepts(route)
|| INDEXERS_BLOCKS.contains(&active_radarr_block);
}
false
@@ -45,6 +50,7 @@ impl DrawUi for IndexersUi {
};
match route {
_ if EditIndexerUi::accepts(route) => EditIndexerUi::draw(f, app, content_rect),
_ if IndexerSettingsUi::accepts(route) => IndexerSettingsUi::draw(f, app, content_rect),
_ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, content_rect),
Route::Radarr(active_radarr_block, _) if INDEXERS_BLOCKS.contains(&active_radarr_block) => {
@@ -69,13 +75,15 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
"Automatic Search",
"Interactive Search",
"Priority",
"Tags",
],
constraints: vec![
Constraint::Ratio(1, 5),
Constraint::Ratio(1, 5),
Constraint::Ratio(1, 5),
Constraint::Ratio(1, 5),
Constraint::Ratio(1, 5),
Constraint::Percentage(25),
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(23),
],
help: app
.data
@@ -90,6 +98,7 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
enable_automatic_search,
enable_interactive_search,
priority,
tags,
..
} = indexer;
let bool_to_text = |flag: bool| {
@@ -112,6 +121,19 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
bool_to_text(*enable_interactive_search);
let mut interactive_search = Text::from(interactive_search_text);
interactive_search.patch_style(interactive_search_style);
let tags: String = tags
.iter()
.map(|tag_id| {
app
.data
.radarr_data
.tags_map
.get_by_left(&tag_id.as_i64().unwrap())
.unwrap()
.clone()
})
.collect::<Vec<String>>()
.join(", ");
Row::new(vec![
Cell::from(name.clone().unwrap_or_default()),
@@ -119,6 +141,7 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Cell::from(automatic_search),
Cell::from(interactive_search),
Cell::from(priority.to_string()),
Cell::from(tags),
])
.style(style_primary())
},
+29 -19
View File
@@ -19,7 +19,8 @@ use crate::ui::utils::{
use crate::ui::{
draw_button, draw_drop_down_menu_button, draw_drop_down_popup, draw_error_popup,
draw_error_popup_over, draw_large_popup_over, draw_medium_popup_over, draw_selectable_list,
draw_table, draw_text_box, draw_text_box_with_label, DrawUi, TableProps,
draw_table, draw_text_box, draw_text_box_with_label, DrawUi, LabeledTextBoxProps, TableProps,
TextBoxProps,
};
use crate::utils::convert_runtime;
use crate::App;
@@ -135,12 +136,15 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
ActiveRadarrBlock::AddMovieSearchInput => {
draw_text_box(
f,
chunks[0],
Some("Add Movie"),
block_content,
offset,
true,
false,
TextBoxProps {
text_box_area: chunks[0],
block_title: Some("Add Movie"),
block_content,
offset,
should_show_cursor: true,
is_selected: false,
cursor_after_string: true,
},
);
f.render_widget(layout_block(), chunks[1]);
@@ -267,12 +271,15 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
draw_text_box(
f,
chunks[0],
Some("Add Movie"),
block_content,
offset,
false,
false,
TextBoxProps {
text_box_area: chunks[0],
block_title: Some("Add Movie"),
block_content,
offset,
should_show_cursor: false,
is_selected: false,
cursor_after_string: true,
},
);
}
@@ -441,12 +448,15 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: R
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
draw_text_box_with_label(
f,
chunks[5],
"Tags",
&tags.text,
*tags.offset.borrow(),
selected_block == &ActiveRadarrBlock::AddMovieTagsInput,
active_radarr_block == ActiveRadarrBlock::AddMovieTagsInput,
LabeledTextBoxProps {
area: chunks[5],
label: "Tags",
text: &tags.text,
offset: *tags.offset.borrow(),
is_selected: selected_block == &ActiveRadarrBlock::AddMovieTagsInput,
should_show_cursor: active_radarr_block == ActiveRadarrBlock::AddMovieTagsInput,
cursor_after_string: true,
},
);
}
+19 -13
View File
@@ -17,7 +17,7 @@ use crate::ui::utils::{
use crate::ui::{
draw_button, draw_checkbox_with_label, draw_drop_down_menu_button, draw_drop_down_popup,
draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over, draw_popup,
draw_selectable_list, draw_text_box_with_label, DrawUi,
draw_selectable_list, draw_text_box_with_label, DrawUi, LabeledTextBoxProps,
};
#[cfg(test)]
@@ -179,21 +179,27 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, pro
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
draw_text_box_with_label(
f,
chunks[4],
"Path",
&path.text,
*path.offset.borrow(),
selected_block == &ActiveRadarrBlock::EditMoviePathInput,
active_radarr_block == ActiveRadarrBlock::EditMoviePathInput,
LabeledTextBoxProps {
area: chunks[4],
label: "Path",
text: &path.text,
offset: *path.offset.borrow(),
is_selected: selected_block == &ActiveRadarrBlock::EditMoviePathInput,
should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditMoviePathInput,
cursor_after_string: true,
},
);
draw_text_box_with_label(
f,
chunks[5],
"Tags",
&tags.text,
*tags.offset.borrow(),
selected_block == &ActiveRadarrBlock::EditMovieTagsInput,
active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput,
LabeledTextBoxProps {
area: chunks[5],
label: "Tags",
text: &tags.text,
offset: *tags.offset.borrow(),
is_selected: selected_block == &ActiveRadarrBlock::EditMovieTagsInput,
should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput,
cursor_after_string: true,
},
);
}
+12 -2
View File
@@ -267,8 +267,18 @@ pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge<'_> {
.label(Line::from(format!("{title}: {:.0}%", ratio * 100.0)))
}
pub fn show_cursor(f: &mut Frame<'_>, area: Rect, offset: usize, string: &str) {
f.set_cursor(area.x + (string.len() - offset) as u16 + 1, area.y + 1);
pub fn show_cursor(
f: &mut Frame<'_>,
area: Rect,
offset: usize,
string: &str,
cursor_after_string: bool,
) {
if cursor_after_string {
f.set_cursor(area.x + (string.len() - offset) as u16 + 1, area.y + 1);
} else {
f.set_cursor(area.x + 1u16, area.y + 1);
}
}
pub fn get_width_from_percentage(area: Rect, percentage: u16) -> usize {