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
Generated
+16 -9
View File
@@ -246,13 +246,13 @@ dependencies = [
[[package]] [[package]]
name = "confy" name = "confy"
version = "0.5.1" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e37668cb35145dcfaa1931a5f37fde375eeae8068b4c0d2f289da28a270b2d2c" checksum = "15d296c475c6ed4093824c28e222420831d27577aaaf0a1163a3b7fc35b248a5"
dependencies = [ dependencies = [
"directories", "directories",
"serde", "serde",
"serde_yaml 0.8.26", "serde_yaml 0.9.29",
"thiserror", "thiserror",
] ]
@@ -331,22 +331,23 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]] [[package]]
name = "directories" name = "directories"
version = "4.0.1" version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
dependencies = [ dependencies = [
"dirs-sys", "dirs-sys",
] ]
[[package]] [[package]]
name = "dirs-sys" name = "dirs-sys"
version = "0.3.7" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [ dependencies = [
"libc", "libc",
"option-ext",
"redox_users", "redox_users",
"winapi", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@@ -860,7 +861,7 @@ dependencies = [
[[package]] [[package]]
name = "managarr" name = "managarr"
version = "0.0.30" version = "0.0.31"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"backtrace", "backtrace",
@@ -1047,6 +1048,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "ordered-float" name = "ordered-float"
version = "2.10.1" version = "2.10.1"
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "managarr" name = "managarr"
version = "0.0.30" version = "0.0.31"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"] authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI to manage your Servarrs" description = "A TUI to manage your Servarrs"
keywords = ["managarr", "tui-rs", "dashboard", "servarr", "tui"] keywords = ["managarr", "tui-rs", "dashboard", "servarr", "tui"]
@@ -9,14 +9,14 @@ repository = "https://github.com/Dark-Alex-17/managarr"
homepage = "https://github.com/Dark-Alex-17/managarr" homepage = "https://github.com/Dark-Alex-17/managarr"
readme = "README.md" readme = "README.md"
edition = "2021" edition = "2021"
rust-version = "1.72.0" rust-version = "1.75.0"
[dependencies] [dependencies]
anyhow = "1.0.68" anyhow = "1.0.68"
backtrace = "0.3.67" backtrace = "0.3.67"
bimap = "0.6.3" bimap = "0.6.3"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
confy = { version = "0.5.1", default_features = false, features = ["yaml_conf"] } confy = { version = "0.6.0", default_features = false, features = ["yaml_conf"] }
crossterm = "0.27.0" crossterm = "0.27.0"
derivative = "2.2.0" derivative = "2.2.0"
human-panic = "1.1.3" human-panic = "1.1.3"
+4 -5
View File
@@ -5,8 +5,8 @@ Managarr is a TUI to help you manage your HTPC (Home Theater PC). Built with lov
## NOTE: Managarr is not yet stable (Pre-Alpha) ## NOTE: Managarr is not yet stable (Pre-Alpha)
I'm regularly making changes to get Managarr to an alpha release. As such, I'm regularly refactoring the code to be cleaner I'm regularly making changes to get Managarr to an alpha release. As such, I'm regularly refactoring the code to be cleaner
and more easily extensible. This makes contributing difficult and until I get Managarr across the alpha-release finish line, and more easily extensible. Until I get Managarr across the alpha-release finish line, this regular refactoring will make
contributions will likely be difficult. Thus, stability is not guaranteed (yet!). contributions difficult. Thus, stability is not guaranteed (yet!).
This means that while all tests will pass, there may be certain menus or keymappings that are no-ops, or produce empty This means that while all tests will pass, there may be certain menus or keymappings that are no-ops, or produce empty
screens, or things of this sort. screens, or things of this sort.
@@ -40,16 +40,15 @@ pleasant as possible!
- [x] View details of a specific movie including description, history, downloaded file info, or the credits - [x] View details of a specific movie including description, history, downloaded file info, or the credits
- [x] View details of any collection and the movies in them - [x] View details of any collection and the movies in them
- [x] Search your library or collections - [x] Search your library or collections
- [x] Add or delete movies and downloads - [x] Add or delete movies, downloads, and indexers
- [x] Trigger automatic searches for movies - [x] Trigger automatic searches for movies
- [x] Trigger refresh and disk scan for movies, downloads, and collections - [x] Trigger refresh and disk scan for movies, downloads, and collections
- [x] Manually search for movies - [x] Manually search for movies
- [x] Edit your movies and collections - [x] Edit your movies, collections, and indexers
- [x] Manage your tags - [x] Manage your tags
- [x] Manage your root folders - [x] Manage your root folders
- [ ] Manage your quality profiles - [ ] Manage your quality profiles
- [ ] Manage your quality definitions - [ ] Manage your quality definitions
- [ ] Manage your indexers
- [x] View and browse logs, tasks, events queues, and updates - [x] View and browse logs, tasks, events queues, and updates
- [x] Manually trigger scheduled tasks - [x] Manually trigger scheduled tasks
+1 -1
View File
@@ -52,7 +52,7 @@ pub static ROOT_FOLDERS_CONTEXT_CLUES: [ContextClue; 3] = [
pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [ pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), (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,
DEFAULT_KEYBINDINGS.settings.desc, DEFAULT_KEYBINDINGS.settings.desc,
+2 -2
View File
@@ -151,8 +151,8 @@ mod tests {
let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit); assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc); assert_str_eq!(*description, "edit indexer");
let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
+1 -4
View File
@@ -1,7 +1,4 @@
pub use self::{ pub use self::key::Key;
input_event::{Events, InputEvent},
key::Key,
};
pub mod input_event; pub mod input_event;
mod key; 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(); let indexer_settings = self.app.data.radarr_data.indexer_settings.as_mut().unwrap();
match self.active_radarr_block { match self.active_radarr_block {
ActiveRadarrBlock::IndexerSettingsPrompt => { ActiveRadarrBlock::IndexerSettingsPrompt => {
self.app.data.radarr_data.selected_block.previous() self.app.data.radarr_data.selected_block.previous();
} }
ActiveRadarrBlock::IndexerSettingsMinimumAgeInput => { ActiveRadarrBlock::IndexerSettingsMinimumAgeInput => {
indexer_settings.minimum_age += 1; indexer_settings.minimum_age += 1;
@@ -166,7 +166,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl
ActiveRadarrBlock::IndexerSettingsConfirmPrompt => { ActiveRadarrBlock::IndexerSettingsConfirmPrompt => {
let radarr_data = &mut self.app.data.radarr_data; let radarr_data = &mut self.app.data.radarr_data;
if radarr_data.prompt_confirm { 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; self.app.should_refresh = true;
} else { } else {
radarr_data.indexer_settings = None; radarr_data.indexer_settings = None;
@@ -456,7 +456,7 @@ mod tests {
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into());
assert_eq!( assert_eq!(
app.data.radarr_data.prompt_confirm_action, app.data.radarr_data.prompt_confirm_action,
Some(RadarrEvent::UpdateIndexerSettings) Some(RadarrEvent::EditAllIndexerSettings)
); );
assert!(app.data.radarr_data.indexer_settings.is_some()); assert!(app.data.radarr_data.indexer_settings.is_some());
assert!(app.should_refresh); assert!(app.should_refresh);
@@ -7,10 +7,10 @@ mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App; use crate::app::App;
use crate::event::Key; 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::handlers::KeyEventHandler;
use crate::models::servarr_data::radarr::radarr_data::{ 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; use crate::test_handler_delegation;
@@ -147,7 +147,14 @@ mod tests {
} }
mod test_handle_submit { 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 pretty_assertions::assert_eq;
use serde_json::{Number, Value};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
@@ -155,16 +162,85 @@ mod tests {
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test] #[rstest]
fn test_indexer_submit_aka_edit() { fn test_edit_indexer_submit(#[values(true, false)] torrent_protocol: bool) {
let mut app = App::default(); 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(); IndexersHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle();
assert_eq!( assert_eq!(
app.get_current_route(), 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] #[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] #[rstest]
fn test_delegates_indexer_settings_blocks_to_indexer_settings_handler( fn test_delegates_indexer_settings_blocks_to_indexer_settings_handler(
#[values( #[values(
@@ -348,7 +447,7 @@ mod tests {
#[test] #[test]
fn test_delegates_test_all_indexers_block_to_test_all_indexers_handler() { fn test_delegates_test_all_indexers_block_to_test_all_indexers_handler() {
test_handler_delegation!( test_handler_delegation!(
TestAllIndexersHandler, IndexersHandler,
ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Indexers,
ActiveRadarrBlock::TestAllIndexers ActiveRadarrBlock::TestAllIndexers
); );
@@ -359,6 +458,8 @@ mod tests {
let mut indexers_blocks = Vec::new(); let mut indexers_blocks = Vec::new();
indexers_blocks.extend(INDEXERS_BLOCKS); indexers_blocks.extend(INDEXERS_BLOCKS);
indexers_blocks.extend(INDEXER_SETTINGS_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| { ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if indexers_blocks.contains(&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::app::App;
use crate::event::Key; use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; 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::edit_indexer_settings_handler::IndexerSettingsHandler;
use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler; use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::radarr::radarr_data::{ 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::models::{BlockSelectionState, Scrollable};
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
mod edit_indexer_handler;
mod edit_indexer_settings_handler; mod edit_indexer_settings_handler;
mod test_all_indexers_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> { impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, 'b> {
fn handle(&mut self) { fn handle(&mut self) {
match self.active_radarr_block { 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) => { _ if IndexerSettingsHandler::accepts(self.active_radarr_block) => {
IndexerSettingsHandler::with(self.key, self.app, self.active_radarr_block, self.context) IndexerSettingsHandler::with(self.key, self.app, self.active_radarr_block, self.context)
.handle() .handle()
@@ -41,7 +48,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
} }
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { 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( fn with(
@@ -115,7 +125,22 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a,
ActiveRadarrBlock::Indexers => { ActiveRadarrBlock::Indexers => {
self self
.app .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, pub priority: i64,
#[serde(deserialize_with = "super::from_i64")] #[serde(deserialize_with = "super::from_i64")]
pub download_client_id: i64, pub download_client_id: i64,
pub tags: Option<Vec<String>>, pub tags: Vec<Number>,
} }
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] #[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct IndexerField { pub struct IndexerField {
#[serde(deserialize_with = "super::from_i64")]
pub order: i64,
pub name: Option<String>, pub name: Option<String>,
pub label: Option<String>,
pub value: Option<Value>, 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)] #[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
+92 -2
View File
@@ -1,6 +1,6 @@
use crate::models::radarr_models::{ use crate::models::radarr_models::{
Collection, Credit, MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release, ReleaseField, Collection, Credit, Indexer, MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release,
RootFolder, ReleaseField, RootFolder,
}; };
use crate::models::servarr_data::radarr::radarr_data::RadarrData; use crate::models::servarr_data::radarr::radarr_data::RadarrData;
use crate::models::{HorizontallyScrollableText, ScrollableText, StatefulList, StatefulTable}; use crate::models::{HorizontallyScrollableText, ScrollableText, StatefulList, StatefulTable};
@@ -24,6 +24,96 @@ pub struct MovieDetailsModal {
pub sort_ascending: Option<bool>, 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)] #[derive(Default)]
pub struct EditMovieModal { pub struct EditMovieModal {
pub minimum_availability_list: StatefulList<MinimumAvailability>, pub minimum_availability_list: StatefulList<MinimumAvailability>,
+99 -3
View File
@@ -1,8 +1,10 @@
#[cfg(test)] #[cfg(test)]
mod 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::{ 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::radarr_test_utils::utils::create_test_radarr_data;
use crate::models::servarr_data::radarr::radarr_data::RadarrData; use crate::models::servarr_data::radarr::radarr_data::RadarrData;
@@ -10,9 +12,103 @@ mod test {
use bimap::BiMap; use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest; use rstest::rstest;
use serde_json::Number; use serde_json::{Number, Value};
use strum::IntoEnumIterator; 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] #[rstest]
fn test_edit_movie_modal_from_radarr_data(#[values(true, false)] test_filtered_movies: bool) { fn test_edit_movie_modal_from_radarr_data(#[values(true, false)] test_filtered_movies: bool) {
let mut radarr_data = RadarrData { let mut radarr_data = RadarrData {
+51 -5
View File
@@ -10,7 +10,8 @@ use crate::models::radarr_models::{
IndexerSettings, Movie, QueueEvent, RootFolder, Task, IndexerSettings, Movie, QueueEvent, RootFolder, Task,
}; };
use crate::models::servarr_data::radarr::modals::{ use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditMovieModal, IndexerTestResultModalItem, MovieDetailsModal, AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
MovieDetailsModal,
}; };
use crate::models::{ use crate::models::{
BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, StatefulList, BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, StatefulList,
@@ -55,6 +56,7 @@ pub struct RadarrData<'a> {
pub add_searched_movies: Option<StatefulTable<AddMovieSearchResult>>, pub add_searched_movies: Option<StatefulTable<AddMovieSearchResult>>,
pub edit_movie_modal: Option<EditMovieModal>, pub edit_movie_modal: Option<EditMovieModal>,
pub edit_collection_modal: Option<EditCollectionModal>, pub edit_collection_modal: Option<EditCollectionModal>,
pub edit_indexer_modal: Option<EditIndexerModal>,
pub edit_root_folder: Option<HorizontallyScrollableText>, pub edit_root_folder: Option<HorizontallyScrollableText>,
pub filtered_collections: Option<StatefulTable<Collection>>, pub filtered_collections: Option<StatefulTable<Collection>>,
pub filtered_movies: Option<StatefulTable<Movie>>, pub filtered_movies: Option<StatefulTable<Movie>>,
@@ -123,6 +125,7 @@ impl<'a> Default for RadarrData<'a> {
add_searched_movies: None, add_searched_movies: None,
edit_movie_modal: None, edit_movie_modal: None,
edit_collection_modal: None, edit_collection_modal: None,
edit_indexer_modal: None,
edit_root_folder: None, edit_root_folder: None,
filtered_collections: None, filtered_collections: None,
filtered_movies: None, filtered_movies: None,
@@ -252,7 +255,16 @@ pub enum ActiveRadarrBlock {
EditCollectionSelectQualityProfile, EditCollectionSelectQualityProfile,
EditCollectionToggleSearchOnAdd, EditCollectionToggleSearchOnAdd,
EditCollectionToggleMonitored, EditCollectionToggleMonitored,
EditIndexer, EditIndexerPrompt,
EditIndexerConfirmPrompt,
EditIndexerApiKeyInput,
EditIndexerNameInput,
EditIndexerSeedRatioInput,
EditIndexerToggleEnableRss,
EditIndexerToggleEnableAutomaticSearch,
EditIndexerToggleEnableInteractiveSearch,
EditIndexerUrlInput,
EditIndexerTagsInput,
EditMoviePrompt, EditMoviePrompt,
EditMovieConfirmPrompt, EditMovieConfirmPrompt,
EditMoviePathInput, EditMoviePathInput,
@@ -318,12 +330,10 @@ pub static COLLECTIONS_BLOCKS: [ActiveRadarrBlock; 6] = [
ActiveRadarrBlock::FilterCollectionsError, ActiveRadarrBlock::FilterCollectionsError,
ActiveRadarrBlock::UpdateAllCollectionsPrompt, ActiveRadarrBlock::UpdateAllCollectionsPrompt,
]; ];
pub static INDEXERS_BLOCKS: [ActiveRadarrBlock; 5] = [ pub static INDEXERS_BLOCKS: [ActiveRadarrBlock; 3] = [
ActiveRadarrBlock::AddIndexer, ActiveRadarrBlock::AddIndexer,
ActiveRadarrBlock::EditIndexer,
ActiveRadarrBlock::DeleteIndexerPrompt, ActiveRadarrBlock::DeleteIndexerPrompt,
ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Indexers,
ActiveRadarrBlock::TestAllIndexers,
]; ];
pub static ROOT_FOLDERS_BLOCKS: [ActiveRadarrBlock; 3] = [ pub static ROOT_FOLDERS_BLOCKS: [ActiveRadarrBlock; 3] = [
ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::RootFolders,
@@ -416,6 +426,42 @@ pub static DELETE_MOVIE_SELECTION_BLOCKS: [ActiveRadarrBlock; 3] = [
ActiveRadarrBlock::DeleteMovieToggleAddListExclusion, ActiveRadarrBlock::DeleteMovieToggleAddListExclusion,
ActiveRadarrBlock::DeleteMovieConfirmPrompt, 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] = [ pub static INDEXER_SETTINGS_BLOCKS: [ActiveRadarrBlock; 10] = [
ActiveRadarrBlock::IndexerSettingsPrompt, ActiveRadarrBlock::IndexerSettingsPrompt,
ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
@@ -98,6 +98,7 @@ mod tests {
assert!(radarr_data.edit_movie_modal.is_none()); assert!(radarr_data.edit_movie_modal.is_none());
assert!(radarr_data.edit_collection_modal.is_none()); assert!(radarr_data.edit_collection_modal.is_none());
assert!(radarr_data.edit_root_folder.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_collections.is_none());
assert!(radarr_data.filtered_movies.is_none()); assert!(radarr_data.filtered_movies.is_none());
assert!(radarr_data.indexer_settings.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, ActiveRadarrBlock, ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, COLLECTIONS_BLOCKS,
COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS, DELETE_MOVIE_SELECTION_BLOCKS, COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS, DELETE_MOVIE_SELECTION_BLOCKS,
DOWNLOADS_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
EDIT_MOVIE_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS,
INDEXER_SETTINGS_SELECTION_BLOCKS, LIBRARY_BLOCKS, MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, EDIT_MOVIE_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS,
SYSTEM_DETAILS_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, LIBRARY_BLOCKS,
MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
}; };
#[test] #[test]
@@ -299,12 +301,10 @@ mod tests {
#[test] #[test]
fn test_indexers_blocks_contents() { 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::AddIndexer));
assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::EditIndexer));
assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::DeleteIndexerPrompt)); assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::DeleteIndexerPrompt));
assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::Indexers)); assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::Indexers));
assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::TestAllIndexers));
} }
#[test] #[test]
@@ -398,6 +398,25 @@ mod tests {
assert!(DELETE_MOVIE_BLOCKS.contains(&ActiveRadarrBlock::DeleteMovieToggleAddListExclusion)); 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] #[test]
fn test_indexer_settings_blocks_contents() { fn test_indexer_settings_blocks_contents() {
assert_eq!(INDEXER_SETTINGS_BLOCKS.len(), 10); assert_eq!(INDEXER_SETTINGS_BLOCKS.len(), 10);
@@ -542,6 +561,101 @@ mod tests {
assert_eq!(delete_movie_block_iter.next(), None); 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] #[test]
fn test_indexer_settings_selection_blocks_ordering() { fn test_indexer_settings_selection_blocks_ordering() {
let mut indexer_settings_block_iter = INDEXER_SETTINGS_SELECTION_BLOCKS.iter(); 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, Update,
}; };
use crate::models::servarr_data::radarr::modals::{ 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::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText, StatefulTable}; use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText, StatefulTable};
@@ -36,7 +37,9 @@ pub enum RadarrEvent {
DeleteMovie, DeleteMovie,
DeleteRootFolder, DeleteRootFolder,
DownloadRelease, DownloadRelease,
EditAllIndexerSettings,
EditCollection, EditCollection,
EditIndexer,
EditMovie, EditMovie,
GetCollections, GetCollections,
GetDownloads, GetDownloads,
@@ -65,7 +68,6 @@ pub enum RadarrEvent {
UpdateAndScan, UpdateAndScan,
UpdateCollections, UpdateCollections,
UpdateDownloads, UpdateDownloads,
UpdateIndexerSettings,
} }
impl RadarrEvent { impl RadarrEvent {
@@ -73,8 +75,10 @@ impl RadarrEvent {
match self { match self {
RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection", RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection",
RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue", RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue",
RadarrEvent::GetIndexers | RadarrEvent::DeleteIndexer => "/indexer", RadarrEvent::GetIndexers | RadarrEvent::EditIndexer | RadarrEvent::DeleteIndexer => {
RadarrEvent::GetIndexerSettings | RadarrEvent::UpdateIndexerSettings => "/config/indexer", "/indexer"
}
RadarrEvent::GetIndexerSettings | RadarrEvent::EditAllIndexerSettings => "/config/indexer",
RadarrEvent::GetLogs => "/log", RadarrEvent::GetLogs => "/log",
RadarrEvent::AddMovie RadarrEvent::AddMovie
| RadarrEvent::EditMovie | RadarrEvent::EditMovie
@@ -123,7 +127,9 @@ impl<'a, 'b> Network<'a, 'b> {
RadarrEvent::DeleteMovie => self.delete_movie().await, RadarrEvent::DeleteMovie => self.delete_movie().await,
RadarrEvent::DeleteRootFolder => self.delete_root_folder().await, RadarrEvent::DeleteRootFolder => self.delete_root_folder().await,
RadarrEvent::DownloadRelease => self.download_release().await, RadarrEvent::DownloadRelease => self.download_release().await,
RadarrEvent::EditAllIndexerSettings => self.edit_all_indexer_settings().await,
RadarrEvent::EditCollection => self.edit_collection().await, RadarrEvent::EditCollection => self.edit_collection().await,
RadarrEvent::EditIndexer => self.edit_indexer().await,
RadarrEvent::EditMovie => self.edit_movie().await, RadarrEvent::EditMovie => self.edit_movie().await,
RadarrEvent::GetCollections => self.get_collections().await, RadarrEvent::GetCollections => self.get_collections().await,
RadarrEvent::GetDownloads => self.get_downloads().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::UpdateAndScan => self.update_and_scan().await,
RadarrEvent::UpdateCollections => self.update_collections().await, RadarrEvent::UpdateCollections => self.update_collections().await,
RadarrEvent::UpdateDownloads => self.update_downloads().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; .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) { async fn edit_collection(&mut self) {
info!("Editing Radarr collection"); info!("Editing Radarr collection");
@@ -545,6 +581,129 @@ impl<'a, 'b> Network<'a, 'b> {
.await; .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) { async fn edit_movie(&mut self) {
info!("Editing Radarr movie"); info!("Editing Radarr movie");
@@ -663,13 +822,13 @@ impl<'a, 'b> Network<'a, 'b> {
.handle_request::<(), Vec<Credit>>(request_props, |credit_vec, mut app| { .handle_request::<(), Vec<Credit>>(request_props, |credit_vec, mut app| {
let cast_vec: Vec<Credit> = credit_vec let cast_vec: Vec<Credit> = credit_vec
.iter() .iter()
.filter(|&credit| credit.credit_type == CreditType::Cast)
.cloned() .cloned()
.filter(|credit| credit.credit_type == CreditType::Cast)
.collect(); .collect();
let crew_vec: Vec<Credit> = credit_vec let crew_vec: Vec<Credit> = credit_vec
.iter() .iter()
.filter(|&credit| credit.credit_type == CreditType::Crew)
.cloned() .cloned()
.filter(|credit| credit.credit_type == CreditType::Crew)
.collect(); .collect();
if app.data.radarr_data.movie_details_modal.is_none() { if app.data.radarr_data.movie_details_modal.is_none() {
@@ -1500,37 +1659,6 @@ impl<'a, 'b> Network<'a, 'b> {
.await; .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>( async fn radarr_request_props_from<T: Serialize + Debug>(
&self, &self,
resource: &str, resource: &str,
+409 -140
View File
@@ -13,8 +13,8 @@ mod test {
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::models::radarr_models::{ use crate::models::radarr_models::{
CollectionMovie, IndexerField, IndexerSelectOption, Language, MediaInfo, MinimumAvailability, CollectionMovie, IndexerField, Language, MediaInfo, MinimumAvailability, Monitor, MovieFile,
Monitor, MovieFile, Quality, QualityWrapper, Rating, RatingsList, Quality, QualityWrapper, Rating, RatingsList,
}; };
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::{HorizontallyScrollableText, StatefulTable}; use crate::models::{HorizontallyScrollableText, StatefulTable};
@@ -136,7 +136,7 @@ mod test {
#[rstest] #[rstest]
fn test_resource_indexer_settings( fn test_resource_indexer_settings(
#[values(RadarrEvent::GetIndexerSettings, RadarrEvent::UpdateIndexerSettings)] #[values(RadarrEvent::GetIndexerSettings, RadarrEvent::EditAllIndexerSettings)]
event: RadarrEvent, event: RadarrEvent,
) { ) {
assert_str_eq!(event.resource(), "/config/indexer"); assert_str_eq!(event.resource(), "/config/indexer");
@@ -782,45 +782,6 @@ mod test {
async_server.assert_async().await; 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] #[tokio::test]
async fn test_handle_update_collections_event() { async fn test_handle_update_collections_event() {
let (async_server, app_arc, _server) = mock_radarr_api( let (async_server, app_arc, _server) = mock_radarr_api(
@@ -1211,37 +1172,22 @@ mod test {
"name": "Test Indexer", "name": "Test Indexer",
"fields": [ "fields": [
{ {
"order": 0, "name": "baseUrl",
"name": "valueIsString", "value": "https://test.com",
"label": "Value Is String",
"value": "hello",
"type": "textbox",
}, },
{ {
"order": 1, "name": "apiKey",
"name": "emptyValueWithSelectOptions", "value": "",
"label": "Empty Value With Select Options",
"type": "select",
"selectOptions": [
{
"value": -2,
"name": "Original",
"order": 0,
}
]
}, },
{ {
"order": 2, "name": "seedCriteria.seedRatio",
"name": "valueIsAnArray", "value": "1.2",
"label": "Value is an array",
"value": [1, 2],
"type": "select",
}, },
], ],
"implementationName": "Torznab", "implementationName": "Torznab",
"implementation": "Torznab", "implementation": "Torznab",
"configContract": "TorznabSettings", "configContract": "TorznabSettings",
"tags": ["test_tag"], "tags": [1],
"id": 1 "id": 1
}]); }]);
let (async_server, app_arc, _server) = mock_radarr_api( let (async_server, app_arc, _server) = mock_radarr_api(
@@ -2083,66 +2029,42 @@ mod test {
} }
#[tokio::test] #[tokio::test]
async fn test_handle_edit_movie_event() { async fn test_handle_edit_all_indexer_settings_event() {
let mut expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); let indexer_settings_json = json!({
*expected_body.get_mut("monitored").unwrap() = json!(false); "minimumAge": 0,
*expected_body.get_mut("minimumAvailability").unwrap() = json!("announced"); "maximumSize": 0,
*expected_body.get_mut("qualityProfileId").unwrap() = json!(1111); "retention": 0,
*expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); "rssSyncInterval": 60,
*expected_body.get_mut("tags").unwrap() = json!([1, 2]); "preferIndexerFlags": false,
"availabilityDelay": 0,
let resource = format!("{}/1", RadarrEvent::GetMovieDetails.resource()); "allowHardcodedSubs": true,
let (async_details_server, app_arc, mut server) = mock_radarr_api( "whitelistedHardcodedSubs": "",
RequestMethod::Get, "id": 1
});
let (async_server, app_arc, _server) = mock_radarr_api(
RequestMethod::Put,
Some(indexer_settings_json),
None, None,
Some(serde_json::from_str(MOVIE_JSON).unwrap()),
None, None,
&resource, RadarrEvent::EditAllIndexerSettings.resource(),
) )
.await; .await;
let async_edit_server = server
.mock( app_arc.lock().await.data.radarr_data.indexer_settings = Some(indexer_settings());
"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()); 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_server.assert_async().await;
async_edit_server.assert_async().await; assert!(app_arc
.lock()
let app = app_arc.lock().await; .await
assert!(app.data.radarr_data.edit_movie_modal.is_none()); .data
.radarr_data
.indexer_settings
.is_none());
} }
#[tokio::test] #[tokio::test]
@@ -2240,6 +2162,369 @@ mod test {
assert!(app.data.radarr_data.edit_collection_modal.is_none()); 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] #[tokio::test]
async fn test_handle_download_release_event() { async fn test_handle_download_release_event() {
let (async_server, app_arc, _server) = mock_radarr_api( let (async_server, app_arc, _server) = mock_radarr_api(
@@ -2786,36 +3071,20 @@ mod test {
implementation_name: Some("Torznab".to_owned()), implementation_name: Some("Torznab".to_owned()),
implementation: Some("Torznab".to_owned()), implementation: Some("Torznab".to_owned()),
config_contract: Some("TorznabSettings".to_owned()), config_contract: Some("TorznabSettings".to_owned()),
tags: Some(vec!["test_tag".to_owned()]), tags: vec![Number::from(1)],
id: 1, id: 1,
fields: Some(vec![ fields: Some(vec![
IndexerField { IndexerField {
order: 0, name: Some("baseUrl".to_owned()),
name: Some("valueIsString".to_owned()), value: Some(json!("https://test.com")),
label: Some("Value Is String".to_owned()),
value: Some(json!("hello")),
field_type: Some("textbox".to_owned()),
select_options: None,
}, },
IndexerField { IndexerField {
order: 1, name: Some("apiKey".to_owned()),
name: Some("emptyValueWithSelectOptions".to_owned()), value: Some(json!("")),
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,
}]),
}, },
IndexerField { IndexerField {
order: 2, name: Some("seedCriteria.seedRatio".to_owned()),
name: Some("valueIsAnArray".to_owned()), value: Some(json!("1.2")),
label: Some("Value is an array".to_owned()),
value: Some(json!([1, 2])),
field_type: Some("select".to_owned()),
select_options: None,
}, },
]), ]),
} }
+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( pub struct TextBoxProps<'a> {
f: &mut Frame<'_>, pub text_box_area: Rect,
text_box_area: Rect, pub block_title: Option<&'a str>,
block_title: Option<&str>, pub block_content: &'a str,
block_content: &str, pub offset: usize,
offset: usize, pub should_show_cursor: bool,
should_show_cursor: bool, pub is_selected: bool,
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 { let (block, style) = if let Some(title) = block_title {
(title_block_centered(title), style_default()) (title_block_centered(title), style_default())
} else { } else {
@@ -703,19 +714,33 @@ pub fn draw_text_box(
f.render_widget(paragraph, text_box_area); f.render_widget(paragraph, text_box_area);
if should_show_cursor { 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( pub fn draw_text_box_with_label(
f: &mut Frame<'_>, f: &mut Frame<'_>,
area: Rect, labeled_text_box_props: LabeledTextBoxProps<'_>,
label: &str,
text: &str,
offset: usize,
is_selected: bool,
should_show_cursor: bool,
) { ) {
let LabeledTextBoxProps {
area,
label,
text,
offset,
is_selected,
should_show_cursor,
cursor_after_string,
} = labeled_text_box_props;
let horizontal_chunks = horizontal_chunks( let horizontal_chunks = horizontal_chunks(
vec![ vec![
Constraint::Percentage(48), Constraint::Percentage(48),
@@ -734,12 +759,15 @@ pub fn draw_text_box_with_label(
draw_text_box( draw_text_box(
f, f,
horizontal_chunks[1], TextBoxProps {
None, text_box_area: horizontal_chunks[1],
text, block_title: None,
offset, block_content: text,
should_show_cursor, offset,
is_selected, should_show_cursor,
is_selected,
cursor_after_string,
},
); );
} }
@@ -761,12 +789,15 @@ pub fn draw_input_box_popup(
draw_text_box( draw_text_box(
f, f,
chunks[0], TextBoxProps {
Some(box_title), text_box_area: chunks[0],
&box_content.text, block_title: Some(box_title),
*box_content.offset.borrow(), block_content: &box_content.text,
true, offset: *box_content.offset.borrow(),
false, should_show_cursor: true,
is_selected: false,
cursor_after_string: true,
},
); );
let help = Paragraph::new("<esc> cancel") let help = Paragraph::new("<esc> cancel")
@@ -16,7 +16,7 @@ use crate::ui::utils::{
use crate::ui::{ use crate::ui::{
draw_button, draw_checkbox_with_label, draw_drop_down_menu_button, draw_drop_down_popup, 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_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)] #[cfg(test)]
@@ -191,12 +191,16 @@ fn draw_edit_collection_confirmation_prompt(
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
draw_text_box_with_label( draw_text_box_with_label(
f, f,
chunks[4], LabeledTextBoxProps {
"Root Folder", area: chunks[4],
&path.text, label: "Root Folder",
*path.offset.borrow(), text: &path.text,
selected_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput, offset: *path.offset.borrow(),
active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput, 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, vertical_chunks_with_margin,
}; };
use crate::ui::{ 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)] #[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() { if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
draw_text_box_with_label( draw_text_box_with_label(
f, f,
left_chunks[0], LabeledTextBoxProps {
"Minimum Age (minutes) ▴▾", area: left_chunks[0],
&indexer_settings.minimum_age.to_string(), label: "Minimum Age (minutes) ▴▾",
0, text: &indexer_settings.minimum_age.to_string(),
selected_block == &ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, offset: 0,
active_radarr_block == ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsMinimumAgeInput,
should_show_cursor: active_radarr_block
== ActiveRadarrBlock::IndexerSettingsMinimumAgeInput,
cursor_after_string: false,
},
); );
draw_text_box_with_label( draw_text_box_with_label(
f, f,
left_chunks[1], LabeledTextBoxProps {
"Retention (days) ▴▾", area: left_chunks[1],
&indexer_settings.retention.to_string(), label: "Retention (days) ▴▾",
0, text: &indexer_settings.retention.to_string(),
selected_block == &ActiveRadarrBlock::IndexerSettingsRetentionInput, offset: 0,
active_radarr_block == ActiveRadarrBlock::IndexerSettingsRetentionInput, is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsRetentionInput,
should_show_cursor: active_radarr_block
== ActiveRadarrBlock::IndexerSettingsRetentionInput,
cursor_after_string: false,
},
); );
draw_text_box_with_label( draw_text_box_with_label(
f, f,
left_chunks[2], LabeledTextBoxProps {
"Maximum Size (MB) ▴▾", area: left_chunks[2],
&indexer_settings.maximum_size.to_string(), label: "Maximum Size (MB) ▴▾",
0, text: &indexer_settings.maximum_size.to_string(),
selected_block == &ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, offset: 0,
active_radarr_block == ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsMaximumSizeInput,
should_show_cursor: active_radarr_block
== ActiveRadarrBlock::IndexerSettingsMaximumSizeInput,
cursor_after_string: false,
},
); );
draw_text_box_with_label( draw_text_box_with_label(
f, f,
right_chunks[0], LabeledTextBoxProps {
"Availability Delay (days) ▴▾", area: right_chunks[0],
&indexer_settings.availability_delay.to_string(), label: "Availability Delay (days) ▴▾",
0, text: &indexer_settings.availability_delay.to_string(),
selected_block == &ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, offset: 0,
active_radarr_block == ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
should_show_cursor: active_radarr_block
== ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput,
cursor_after_string: false,
},
); );
draw_text_box_with_label( draw_text_box_with_label(
f, f,
right_chunks[1], LabeledTextBoxProps {
"RSS Sync Interval (minutes) ▴▾", area: right_chunks[1],
&indexer_settings.rss_sync_interval.to_string(), label: "RSS Sync Interval (minutes) ▴▾",
0, text: &indexer_settings.rss_sync_interval.to_string(),
selected_block == &ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, offset: 0,
active_radarr_block == ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput,
should_show_cursor: active_radarr_block
== ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput,
cursor_after_string: false,
},
); );
draw_text_box_with_label( draw_text_box_with_label(
f, f,
right_chunks[2], LabeledTextBoxProps {
"Whitelisted Subtitle Tags", area: right_chunks[2],
&indexer_settings.whitelisted_hardcoded_subs.text, label: "Whitelisted Subtitle Tags",
*indexer_settings.whitelisted_hardcoded_subs.offset.borrow(), text: &indexer_settings.whitelisted_hardcoded_subs.text,
selected_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, offset: *indexer_settings.whitelisted_hardcoded_subs.offset.borrow(),
active_radarr_block == ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, 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 strum::IntoEnumIterator;
use crate::models::servarr_data::radarr::radarr_data::{ 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::radarr_ui::indexers::IndexersUi;
use crate::ui::DrawUi; use crate::ui::DrawUi;
@@ -13,6 +13,8 @@ mod tests {
let mut indexers_blocks = Vec::new(); let mut indexers_blocks = Vec::new();
indexers_blocks.extend(INDEXERS_BLOCKS); indexers_blocks.extend(INDEXERS_BLOCKS);
indexers_blocks.extend(INDEXER_SETTINGS_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| { ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if indexers_blocks.contains(&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::radarr_models::Indexer;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, INDEXERS_BLOCKS}; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, INDEXERS_BLOCKS};
use crate::models::Route; 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::indexer_settings_ui::IndexerSettingsUi;
use crate::ui::radarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi; 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::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}; use crate::ui::{draw_prompt_box, draw_prompt_popup_over, draw_table, DrawUi, TableProps};
mod edit_indexer_ui;
mod indexer_settings_ui; mod indexer_settings_ui;
mod test_all_indexers_ui;
#[cfg(test)] #[cfg(test)]
#[path = "indexers_ui_tests.rs"] #[path = "indexers_ui_tests.rs"]
mod indexers_ui_tests; mod indexers_ui_tests;
mod test_all_indexers_ui;
pub(super) struct IndexersUi; pub(super) struct IndexersUi;
impl DrawUi for IndexersUi { impl DrawUi for IndexersUi {
fn accepts(route: Route) -> bool { fn accepts(route: Route) -> bool {
if let Route::Radarr(active_radarr_block, _) = route { 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 false
@@ -45,6 +50,7 @@ impl DrawUi for IndexersUi {
}; };
match route { match route {
_ if EditIndexerUi::accepts(route) => EditIndexerUi::draw(f, app, content_rect),
_ if IndexerSettingsUi::accepts(route) => IndexerSettingsUi::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), _ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, content_rect),
Route::Radarr(active_radarr_block, _) if INDEXERS_BLOCKS.contains(&active_radarr_block) => { 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", "Automatic Search",
"Interactive Search", "Interactive Search",
"Priority", "Priority",
"Tags",
], ],
constraints: vec![ constraints: vec![
Constraint::Ratio(1, 5), Constraint::Percentage(25),
Constraint::Ratio(1, 5), Constraint::Percentage(13),
Constraint::Ratio(1, 5), Constraint::Percentage(13),
Constraint::Ratio(1, 5), Constraint::Percentage(13),
Constraint::Ratio(1, 5), Constraint::Percentage(13),
Constraint::Percentage(23),
], ],
help: app help: app
.data .data
@@ -90,6 +98,7 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
enable_automatic_search, enable_automatic_search,
enable_interactive_search, enable_interactive_search,
priority, priority,
tags,
.. ..
} = indexer; } = indexer;
let bool_to_text = |flag: bool| { 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); bool_to_text(*enable_interactive_search);
let mut interactive_search = Text::from(interactive_search_text); let mut interactive_search = Text::from(interactive_search_text);
interactive_search.patch_style(interactive_search_style); 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![ Row::new(vec![
Cell::from(name.clone().unwrap_or_default()), 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(automatic_search),
Cell::from(interactive_search), Cell::from(interactive_search),
Cell::from(priority.to_string()), Cell::from(priority.to_string()),
Cell::from(tags),
]) ])
.style(style_primary()) .style(style_primary())
}, },
+29 -19
View File
@@ -19,7 +19,8 @@ use crate::ui::utils::{
use crate::ui::{ use crate::ui::{
draw_button, draw_drop_down_menu_button, draw_drop_down_popup, draw_error_popup, 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_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::utils::convert_runtime;
use crate::App; use crate::App;
@@ -135,12 +136,15 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
ActiveRadarrBlock::AddMovieSearchInput => { ActiveRadarrBlock::AddMovieSearchInput => {
draw_text_box( draw_text_box(
f, f,
chunks[0], TextBoxProps {
Some("Add Movie"), text_box_area: chunks[0],
block_content, block_title: Some("Add Movie"),
offset, block_content,
true, offset,
false, should_show_cursor: true,
is_selected: false,
cursor_after_string: true,
},
); );
f.render_widget(layout_block(), chunks[1]); 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( draw_text_box(
f, f,
chunks[0], TextBoxProps {
Some("Add Movie"), text_box_area: chunks[0],
block_content, block_title: Some("Add Movie"),
offset, block_content,
false, offset,
false, 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() { if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
draw_text_box_with_label( draw_text_box_with_label(
f, f,
chunks[5], LabeledTextBoxProps {
"Tags", area: chunks[5],
&tags.text, label: "Tags",
*tags.offset.borrow(), text: &tags.text,
selected_block == &ActiveRadarrBlock::AddMovieTagsInput, offset: *tags.offset.borrow(),
active_radarr_block == ActiveRadarrBlock::AddMovieTagsInput, 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::{ use crate::ui::{
draw_button, draw_checkbox_with_label, draw_drop_down_menu_button, draw_drop_down_popup, 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_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)] #[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() { if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
draw_text_box_with_label( draw_text_box_with_label(
f, f,
chunks[4], LabeledTextBoxProps {
"Path", area: chunks[4],
&path.text, label: "Path",
*path.offset.borrow(), text: &path.text,
selected_block == &ActiveRadarrBlock::EditMoviePathInput, offset: *path.offset.borrow(),
active_radarr_block == ActiveRadarrBlock::EditMoviePathInput, is_selected: selected_block == &ActiveRadarrBlock::EditMoviePathInput,
should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditMoviePathInput,
cursor_after_string: true,
},
); );
draw_text_box_with_label( draw_text_box_with_label(
f, f,
chunks[5], LabeledTextBoxProps {
"Tags", area: chunks[5],
&tags.text, label: "Tags",
*tags.offset.borrow(), text: &tags.text,
selected_block == &ActiveRadarrBlock::EditMovieTagsInput, offset: *tags.offset.borrow(),
active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput, 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))) .label(Line::from(format!("{title}: {:.0}%", ratio * 100.0)))
} }
pub fn show_cursor(f: &mut Frame<'_>, area: Rect, offset: usize, string: &str) { pub fn show_cursor(
f.set_cursor(area.x + (string.len() - offset) as u16 + 1, area.y + 1); 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 { pub fn get_width_from_percentage(area: Rect, percentage: u16) -> usize {