diff --git a/Cargo.lock b/Cargo.lock index 988b6af..4bcd4b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,13 +246,13 @@ dependencies = [ [[package]] name = "confy" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e37668cb35145dcfaa1931a5f37fde375eeae8068b4c0d2f289da28a270b2d2c" +checksum = "15d296c475c6ed4093824c28e222420831d27577aaaf0a1163a3b7fc35b248a5" dependencies = [ "directories", "serde", - "serde_yaml 0.8.26", + "serde_yaml 0.9.29", "thiserror", ] @@ -331,22 +331,23 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "directories" -version = "4.0.1" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.3.7" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", + "option-ext", "redox_users", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -860,7 +861,7 @@ dependencies = [ [[package]] name = "managarr" -version = "0.0.30" +version = "0.0.31" dependencies = [ "anyhow", "backtrace", @@ -1047,6 +1048,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" version = "2.10.1" diff --git a/Cargo.toml b/Cargo.toml index af70bdb..5ece488 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managarr" -version = "0.0.30" +version = "0.0.31" authors = ["Alex Clarke "] description = "A TUI to manage your Servarrs" 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" readme = "README.md" edition = "2021" -rust-version = "1.72.0" +rust-version = "1.75.0" [dependencies] anyhow = "1.0.68" backtrace = "0.3.67" bimap = "0.6.3" 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" derivative = "2.2.0" human-panic = "1.1.3" diff --git a/README.md b/README.md index 573e631..cadc1d8 100644 --- a/README.md +++ b/README.md @@ -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) 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, -contributions will likely be difficult. Thus, stability is not guaranteed (yet!). +and more easily extensible. Until I get Managarr across the alpha-release finish line, this regular refactoring will make +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 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 any collection and the movies in them - [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 refresh and disk scan for movies, downloads, and collections - [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 root folders - [ ] Manage your quality profiles - [ ] Manage your quality definitions -- [ ] Manage your indexers - [x] View and browse logs, tasks, events queues, and updates - [x] Manually trigger scheduled tasks diff --git a/src/app/radarr/radarr_context_clues.rs b/src/app/radarr/radarr_context_clues.rs index bdfbd04..02d340e 100644 --- a/src/app/radarr/radarr_context_clues.rs +++ b/src/app/radarr/radarr_context_clues.rs @@ -52,7 +52,7 @@ pub static ROOT_FOLDERS_CONTEXT_CLUES: [ContextClue; 3] = [ pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [ (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), - (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), + (DEFAULT_KEYBINDINGS.submit, "edit indexer"), ( DEFAULT_KEYBINDINGS.settings, DEFAULT_KEYBINDINGS.settings.desc, diff --git a/src/app/radarr/radarr_context_clues_tests.rs b/src/app/radarr/radarr_context_clues_tests.rs index bbad3c3..b78cdb7 100644 --- a/src/app/radarr/radarr_context_clues_tests.rs +++ b/src/app/radarr/radarr_context_clues_tests.rs @@ -151,8 +151,8 @@ mod tests { let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc); + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "edit indexer"); let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); diff --git a/src/event/mod.rs b/src/event/mod.rs index f7f501b..dee66a6 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -1,7 +1,4 @@ -pub use self::{ - input_event::{Events, InputEvent}, - key::Key, -}; +pub use self::key::Key; pub mod input_event; mod key; diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs new file mode 100644 index 0000000..63a5c7e --- /dev/null +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler.rs @@ -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, +} + +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, + ) -> 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 + ); + } + _ => (), + } + } +} diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs new file mode 100644 index 0000000..170489a --- /dev/null +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_handler_tests.rs @@ -0,0 +1,1521 @@ +#[cfg(test)] +mod tests { + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::event::Key; + use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS}; + use strum::IntoEnumIterator; + + mod test_handle_scroll_up_and_down { + use crate::app::App; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::servarr_data::radarr::modals::EditIndexerModal; + use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS; + use crate::models::BlockSelectionState; + + use super::*; + + #[rstest] + fn test_edit_indexer_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.next(); + + EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) + .handle(); + + if key == Key::Up { + assert_eq!( + app.data.radarr_data.selected_block.get_active_block(), + &ActiveRadarrBlock::EditIndexerNameInput + ); + } else { + assert_eq!( + app.data.radarr_data.selected_block.get_active_block(), + &ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch + ); + } + } + } + + mod test_handle_home_end { + use crate::app::App; + use crate::models::servarr_data::radarr::modals::EditIndexerModal; + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_edit_indexer_name_input_home_end() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.home.key, + &mut app, + &ActiveRadarrBlock::EditIndexerNameInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .borrow(), + 4 + ); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.end.key, + &mut app, + &ActiveRadarrBlock::EditIndexerNameInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .borrow(), + 0 + ); + } + + #[test] + fn test_edit_indexer_url_input_home_end() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.home.key, + &mut app, + &ActiveRadarrBlock::EditIndexerUrlInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .borrow(), + 4 + ); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.end.key, + &mut app, + &ActiveRadarrBlock::EditIndexerUrlInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .borrow(), + 0 + ); + } + + #[test] + fn test_edit_indexer_api_key_input_home_end() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.home.key, + &mut app, + &ActiveRadarrBlock::EditIndexerApiKeyInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .borrow(), + 4 + ); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.end.key, + &mut app, + &ActiveRadarrBlock::EditIndexerApiKeyInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .borrow(), + 0 + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_home_end() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.home.key, + &mut app, + &ActiveRadarrBlock::EditIndexerSeedRatioInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .borrow(), + 4 + ); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.end.key, + &mut app, + &ActiveRadarrBlock::EditIndexerSeedRatioInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .borrow(), + 0 + ); + } + + #[test] + fn test_edit_indexer_tags_input_home_end() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.home.key, + &mut app, + &ActiveRadarrBlock::EditIndexerTagsInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .borrow(), + 4 + ); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.end.key, + &mut app, + &ActiveRadarrBlock::EditIndexerTagsInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .borrow(), + 0 + ); + } + } + + mod test_handle_left_right_action { + use crate::app::App; + use crate::models::servarr_data::radarr::modals::EditIndexerModal; + use crate::models::servarr_data::radarr::radarr_data::{ + EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use crate::models::BlockSelectionState; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::default(); + app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.index = EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1; + + EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) + .handle(); + + assert!(app.data.radarr_data.prompt_confirm); + + EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) + .handle(); + + assert!(!app.data.radarr_data.prompt_confirm); + } + + #[rstest] + #[case( + 0, + ActiveRadarrBlock::EditIndexerNameInput, + ActiveRadarrBlock::EditIndexerUrlInput + )] + #[case( + 1, + ActiveRadarrBlock::EditIndexerToggleEnableRss, + ActiveRadarrBlock::EditIndexerApiKeyInput + )] + #[case( + 2, + ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveRadarrBlock::EditIndexerSeedRatioInput + )] + #[case( + 3, + ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveRadarrBlock::EditIndexerTagsInput + )] + fn test_left_right_block_toggle_torrents( + #[values(Key::Left, Key::Right)] key: Key, + #[case] starting_index: usize, + #[case] left_block: ActiveRadarrBlock, + #[case] right_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.index = starting_index; + + assert_eq!( + app.data.radarr_data.selected_block.get_active_block(), + &left_block + ); + + EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) + .handle(); + + assert_eq!( + app.data.radarr_data.selected_block.get_active_block(), + &right_block + ); + + EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) + .handle(); + + assert_eq!( + app.data.radarr_data.selected_block.get_active_block(), + &left_block + ); + } + + #[rstest] + #[case( + 0, + ActiveRadarrBlock::EditIndexerNameInput, + ActiveRadarrBlock::EditIndexerUrlInput + )] + #[case( + 1, + ActiveRadarrBlock::EditIndexerToggleEnableRss, + ActiveRadarrBlock::EditIndexerApiKeyInput + )] + #[case( + 2, + ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveRadarrBlock::EditIndexerTagsInput + )] + fn test_left_right_block_toggle_nzb( + #[values(Key::Left, Key::Right)] key: Key, + #[case] starting_index: usize, + #[case] left_block: ActiveRadarrBlock, + #[case] right_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_NZB_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.index = starting_index; + + assert_eq!( + app.data.radarr_data.selected_block.get_active_block(), + &left_block + ); + + EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) + .handle(); + + assert_eq!( + app.data.radarr_data.selected_block.get_active_block(), + &right_block + ); + + EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) + .handle(); + + assert_eq!( + app.data.radarr_data.selected_block.get_active_block(), + &left_block + ); + } + + #[rstest] + fn test_left_right_block_toggle_nzb_empty_row_to_prompt_confirm( + #[values(Key::Left, Key::Right)] key: Key, + ) { + let mut app = App::default(); + app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_NZB_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.index = 3; + app.data.radarr_data.prompt_confirm = false; + + assert_eq!( + app.data.radarr_data.selected_block.get_active_block(), + &ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch + ); + + EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) + .handle(); + + assert_eq!( + app.data.radarr_data.selected_block.get_active_block(), + &ActiveRadarrBlock::EditIndexerConfirmPrompt + ); + + EditIndexerHandler::with(&key, &mut app, &ActiveRadarrBlock::EditIndexerPrompt, &None) + .handle(); + + assert_eq!( + app.data.radarr_data.selected_block.get_active_block(), + &ActiveRadarrBlock::EditIndexerConfirmPrompt + ); + assert!(app.data.radarr_data.prompt_confirm); + } + + #[test] + fn test_edit_indexer_name_input_left_right_keys() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.left.key, + &mut app, + &ActiveRadarrBlock::EditIndexerNameInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .borrow(), + 1 + ); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.right.key, + &mut app, + &ActiveRadarrBlock::EditIndexerNameInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .offset + .borrow(), + 0 + ); + } + + #[test] + fn test_edit_indexer_url_input_left_right_keys() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.left.key, + &mut app, + &ActiveRadarrBlock::EditIndexerUrlInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .borrow(), + 1 + ); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.right.key, + &mut app, + &ActiveRadarrBlock::EditIndexerUrlInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .offset + .borrow(), + 0 + ); + } + + #[test] + fn test_edit_indexer_api_key_input_left_right_keys() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.left.key, + &mut app, + &ActiveRadarrBlock::EditIndexerApiKeyInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .borrow(), + 1 + ); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.right.key, + &mut app, + &ActiveRadarrBlock::EditIndexerApiKeyInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .offset + .borrow(), + 0 + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_left_right_keys() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.left.key, + &mut app, + &ActiveRadarrBlock::EditIndexerSeedRatioInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .borrow(), + 1 + ); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.right.key, + &mut app, + &ActiveRadarrBlock::EditIndexerSeedRatioInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .offset + .borrow(), + 0 + ); + } + + #[test] + fn test_edit_indexer_tags_input_left_right_keys() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.left.key, + &mut app, + &ActiveRadarrBlock::EditIndexerTagsInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .borrow(), + 1 + ); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.right.key, + &mut app, + &ActiveRadarrBlock::EditIndexerTagsInput, + &None, + ) + .handle(); + + assert_eq!( + *app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .offset + .borrow(), + 0 + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::app::App; + use crate::models::servarr_data::radarr::modals::EditIndexerModal; + use crate::models::{ + servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, BlockSelectionState, + }; + use crate::network::radarr_network::RadarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_edit_indexer_prompt_prompt_decline_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .radarr_data + .selected_block + .set_index(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerPrompt, + &None, + ) + .handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert_eq!(app.data.radarr_data.prompt_confirm_action, None); + assert!(!app.should_refresh); + assert_eq!(app.data.radarr_data.edit_indexer_modal, None); + } + + #[test] + fn test_edit_indexer_prompt_prompt_confirmation_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .radarr_data + .selected_block + .set_index(EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.len() - 1); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.radarr_data.prompt_confirm = true; + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerPrompt, + &None, + ) + .handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert!(app.data.radarr_data.edit_indexer_modal.is_some()); + assert!(app.should_refresh); + assert_eq!( + app.data.radarr_data.prompt_confirm_action, + Some(RadarrEvent::EditIndexer) + ); + } + + #[rstest] + #[case(0, ActiveRadarrBlock::EditIndexerNameInput)] + #[case(5, ActiveRadarrBlock::EditIndexerUrlInput)] + #[case(6, ActiveRadarrBlock::EditIndexerApiKeyInput)] + #[case(7, ActiveRadarrBlock::EditIndexerSeedRatioInput)] + #[case(8, ActiveRadarrBlock::EditIndexerTagsInput)] + fn test_edit_indexer_prompt_submit_input_fields( + #[case] starting_index: usize, + #[case] block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app + .data + .radarr_data + .selected_block + .set_index(starting_index); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerPrompt, + &None, + ) + .handle(); + + assert_eq!(app.get_current_route(), &block.into()); + assert!(app.should_ignore_quit_key); + } + + #[test] + fn test_edit_indexer_toggle_enable_rss_submit() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(1); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditIndexerPrompt.into() + ); + assert!(app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_rss + .unwrap()); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditIndexerPrompt.into() + ); + assert!(!app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_rss + .unwrap()); + } + + #[test] + fn test_edit_indexer_toggle_enable_automatic_search_submit() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(2); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditIndexerPrompt.into() + ); + assert!(app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_automatic_search + .unwrap()); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditIndexerPrompt.into() + ); + assert!(!app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_automatic_search + .unwrap()); + } + + #[test] + fn test_edit_indexer_toggle_enable_interactive_search_submit() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + app.data.radarr_data.selected_block.set_index(3); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditIndexerPrompt.into() + ); + assert!(app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_interactive_search + .unwrap()); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditIndexerPrompt.into() + ); + assert!(!app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .enable_interactive_search + .unwrap()); + } + + #[test] + fn test_edit_indexer_name_input_submit() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerNameInput.into()); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerNameInput, + &None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text + .is_empty()); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditIndexerPrompt.into() + ); + } + + #[test] + fn test_edit_indexer_url_input_submit() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerUrlInput.into()); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerUrlInput, + &None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text + .is_empty()); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditIndexerPrompt.into() + ); + } + + #[test] + fn test_edit_indexer_api_key_input_submit() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerApiKeyInput.into()); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerApiKeyInput, + &None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text + .is_empty()); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditIndexerPrompt.into() + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_submit() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerSeedRatioInput.into()); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerSeedRatioInput, + &None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text + .is_empty()); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditIndexerPrompt.into() + ); + } + + #[test] + fn test_edit_indexer_tags_input_submit() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerTagsInput.into()); + + EditIndexerHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerTagsInput, + &None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(!app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text + .is_empty()); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::EditIndexerPrompt.into() + ); + } + } + + mod test_handle_esc { + use super::*; + use crate::app::App; + use crate::event::Key; + use crate::models::servarr_data::radarr::modals::EditIndexerModal; + use pretty_assertions::assert_eq; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_edit_indexer_prompt_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); + app.push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + &ESC_KEY, + &mut app, + &ActiveRadarrBlock::EditIndexerPrompt, + &None, + ) + .handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.data.radarr_data.edit_indexer_modal, None); + } + + #[rstest] + fn test_edit_indexer_input_fields_esc( + #[values( + ActiveRadarrBlock::EditIndexerNameInput, + ActiveRadarrBlock::EditIndexerUrlInput, + ActiveRadarrBlock::EditIndexerApiKeyInput, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + ActiveRadarrBlock::EditIndexerTagsInput + )] + active_radarr_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Indexers.into()); + app.push_navigation_stack(active_radarr_block.into()); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + app.should_ignore_quit_key = true; + + EditIndexerHandler::with(&ESC_KEY, &mut app, &active_radarr_block, &None).handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); + assert!(!app.should_ignore_quit_key); + assert_eq!( + app.data.radarr_data.edit_indexer_modal, + Some(EditIndexerModal::default()) + ); + } + } + + mod test_handle_key_char { + use crate::app::App; + use crate::models::servarr_data::radarr::modals::EditIndexerModal; + use pretty_assertions::assert_str_eq; + + use super::*; + + #[test] + fn test_edit_indexer_name_input_backspace() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + name: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + &ActiveRadarrBlock::EditIndexerNameInput, + &None, + ) + .handle(); + + assert_str_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_url_input_backspace() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + url: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + &ActiveRadarrBlock::EditIndexerUrlInput, + &None, + ) + .handle(); + + assert_str_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_api_key_input_backspace() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + api_key: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + &ActiveRadarrBlock::EditIndexerApiKeyInput, + &None, + ) + .handle(); + + assert_str_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_backspace() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + seed_ratio: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + &ActiveRadarrBlock::EditIndexerSeedRatioInput, + &None, + ) + .handle(); + + assert_str_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_tags_input_backspace() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal { + tags: "Test".into(), + ..EditIndexerModal::default() + }); + + EditIndexerHandler::with( + &DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + &ActiveRadarrBlock::EditIndexerTagsInput, + &None, + ) + .handle(); + + assert_str_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text, + "Tes" + ); + } + + #[test] + fn test_edit_indexer_name_input_char_key() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + &Key::Char('h'), + &mut app, + &ActiveRadarrBlock::EditIndexerNameInput, + &None, + ) + .handle(); + + assert_str_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .name + .text, + "h" + ); + } + + #[test] + fn test_edit_indexer_url_input_char_key() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + &Key::Char('h'), + &mut app, + &ActiveRadarrBlock::EditIndexerUrlInput, + &None, + ) + .handle(); + + assert_str_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .url + .text, + "h" + ); + } + + #[test] + fn test_edit_indexer_api_key_input_char_key() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + &Key::Char('h'), + &mut app, + &ActiveRadarrBlock::EditIndexerApiKeyInput, + &None, + ) + .handle(); + + assert_str_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .api_key + .text, + "h" + ); + } + + #[test] + fn test_edit_indexer_seed_ratio_input_char_key() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + &Key::Char('h'), + &mut app, + &ActiveRadarrBlock::EditIndexerSeedRatioInput, + &None, + ) + .handle(); + + assert_str_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .seed_ratio + .text, + "h" + ); + } + + #[test] + fn test_edit_indexer_tags_input_char_key() { + let mut app = App::default(); + app.data.radarr_data.edit_indexer_modal = Some(EditIndexerModal::default()); + + EditIndexerHandler::with( + &Key::Char('h'), + &mut app, + &ActiveRadarrBlock::EditIndexerTagsInput, + &None, + ) + .handle(); + + assert_str_eq!( + app + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text, + "h" + ); + } + } + + #[test] + fn test_indexer_settings_handler_accepts() { + ActiveRadarrBlock::iter().for_each(|active_radarr_block| { + if EDIT_INDEXER_BLOCKS.contains(&active_radarr_block) { + assert!(EditIndexerHandler::accepts(&active_radarr_block)); + } else { + assert!(!EditIndexerHandler::accepts(&active_radarr_block)); + } + }) + } +} diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs index 3945cb6..b1ba34d 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler.rs @@ -46,7 +46,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl let indexer_settings = self.app.data.radarr_data.indexer_settings.as_mut().unwrap(); match self.active_radarr_block { ActiveRadarrBlock::IndexerSettingsPrompt => { - self.app.data.radarr_data.selected_block.previous() + self.app.data.radarr_data.selected_block.previous(); } ActiveRadarrBlock::IndexerSettingsMinimumAgeInput => { indexer_settings.minimum_age += 1; @@ -166,7 +166,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexerSettingsHandl ActiveRadarrBlock::IndexerSettingsConfirmPrompt => { let radarr_data = &mut self.app.data.radarr_data; if radarr_data.prompt_confirm { - radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateIndexerSettings); + radarr_data.prompt_confirm_action = Some(RadarrEvent::EditAllIndexerSettings); self.app.should_refresh = true; } else { radarr_data.indexer_settings = None; diff --git a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs index f58d9c7..84bd315 100644 --- a/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/edit_indexer_settings_handler_tests.rs @@ -456,7 +456,7 @@ mod tests { assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Indexers.into()); assert_eq!( app.data.radarr_data.prompt_confirm_action, - Some(RadarrEvent::UpdateIndexerSettings) + Some(RadarrEvent::EditAllIndexerSettings) ); assert!(app.data.radarr_data.indexer_settings.is_some()); assert!(app.should_refresh); diff --git a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs index acda375..17cf83c 100644 --- a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs @@ -7,10 +7,10 @@ mod tests { use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; - use crate::handlers::radarr_handlers::indexers::{IndexersHandler, TestAllIndexersHandler}; + use crate::handlers::radarr_handlers::indexers::IndexersHandler; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::radarr::radarr_data::{ - ActiveRadarrBlock, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, + ActiveRadarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, }; use crate::test_handler_delegation; @@ -147,7 +147,14 @@ mod tests { } mod test_handle_submit { + use crate::models::radarr_models::{Indexer, IndexerField}; + use crate::models::servarr_data::radarr::modals::EditIndexerModal; + use crate::models::servarr_data::radarr::radarr_data::{ + RadarrData, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + }; + use bimap::BiMap; use pretty_assertions::assert_eq; + use serde_json::{Number, Value}; use crate::network::radarr_network::RadarrEvent; @@ -155,16 +162,85 @@ mod tests { const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; - #[test] - fn test_indexer_submit_aka_edit() { + #[rstest] + fn test_edit_indexer_submit(#[values(true, false)] torrent_protocol: bool) { let mut app = App::default(); + let protocol = if torrent_protocol { + "torrent".to_owned() + } else { + "usenet".to_owned() + }; + let mut expected_edit_indexer_modal = EditIndexerModal { + name: "Test".into(), + enable_rss: Some(true), + enable_automatic_search: Some(true), + enable_interactive_search: Some(true), + url: "https://test.com".into(), + api_key: "1234".into(), + tags: "usenet, test".into(), + ..EditIndexerModal::default() + }; + let mut radarr_data = RadarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..RadarrData::default() + }; + let mut fields = vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://test.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("1234".to_owned())), + }, + ]; + + if torrent_protocol { + fields.push(IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(Value::from(1.2f64)), + }); + expected_edit_indexer_modal.seed_ratio = "1.2".into(); + } + + let indexer = Indexer { + name: Some("Test".to_owned()), + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + protocol, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + ..Indexer::default() + }; + radarr_data.indexers.set_items(vec![indexer]); + app.data.radarr_data = radarr_data; IndexersHandler::with(&SUBMIT_KEY, &mut app, &ActiveRadarrBlock::Indexers, &None).handle(); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::EditIndexer.into() + &ActiveRadarrBlock::EditIndexerPrompt.into() ); + assert_eq!( + app.data.radarr_data.edit_indexer_modal, + Some((&app.data.radarr_data).into()) + ); + assert_eq!( + app.data.radarr_data.edit_indexer_modal, + Some(expected_edit_indexer_modal) + ); + if torrent_protocol { + assert_eq!( + app.data.radarr_data.selected_block.blocks, + &EDIT_INDEXER_TORRENT_SELECTION_BLOCKS + ); + } else { + assert_eq!( + app.data.radarr_data.selected_block.blocks, + &EDIT_INDEXER_NZB_SELECTION_BLOCKS + ); + } } #[test] @@ -322,6 +398,29 @@ mod tests { } } + #[rstest] + fn test_delegates_edit_indexer_blocks_to_edit_indexer_handler( + #[values( + ActiveRadarrBlock::EditIndexerPrompt, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ActiveRadarrBlock::EditIndexerApiKeyInput, + ActiveRadarrBlock::EditIndexerNameInput, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + ActiveRadarrBlock::EditIndexerToggleEnableRss, + ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveRadarrBlock::EditIndexerUrlInput, + ActiveRadarrBlock::EditIndexerTagsInput + )] + active_radarr_block: ActiveRadarrBlock, + ) { + test_handler_delegation!( + IndexersHandler, + ActiveRadarrBlock::Indexers, + active_radarr_block + ); + } + #[rstest] fn test_delegates_indexer_settings_blocks_to_indexer_settings_handler( #[values( @@ -348,7 +447,7 @@ mod tests { #[test] fn test_delegates_test_all_indexers_block_to_test_all_indexers_handler() { test_handler_delegation!( - TestAllIndexersHandler, + IndexersHandler, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::TestAllIndexers ); @@ -359,6 +458,8 @@ mod tests { let mut indexers_blocks = Vec::new(); indexers_blocks.extend(INDEXERS_BLOCKS); indexers_blocks.extend(INDEXER_SETTINGS_BLOCKS); + indexers_blocks.extend(EDIT_INDEXER_BLOCKS); + indexers_blocks.push(ActiveRadarrBlock::TestAllIndexers); ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if indexers_blocks.contains(&active_radarr_block) { diff --git a/src/handlers/radarr_handlers/indexers/mod.rs b/src/handlers/radarr_handlers/indexers/mod.rs index 6e6a80e..45b2833 100644 --- a/src/handlers/radarr_handlers/indexers/mod.rs +++ b/src/handlers/radarr_handlers/indexers/mod.rs @@ -2,15 +2,18 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::App; use crate::event::Key; use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler; use crate::handlers::radarr_handlers::indexers::edit_indexer_settings_handler::IndexerSettingsHandler; use crate::handlers::radarr_handlers::indexers::test_all_indexers_handler::TestAllIndexersHandler; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::models::servarr_data::radarr::radarr_data::{ - ActiveRadarrBlock, INDEXERS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, + ActiveRadarrBlock, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, + INDEXERS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, }; use crate::models::{BlockSelectionState, Scrollable}; use crate::network::radarr_network::RadarrEvent; +mod edit_indexer_handler; mod edit_indexer_settings_handler; mod test_all_indexers_handler; @@ -28,6 +31,10 @@ pub(super) struct IndexersHandler<'a, 'b> { impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, 'b> { fn handle(&mut self) { match self.active_radarr_block { + _ if EditIndexerHandler::accepts(self.active_radarr_block) => { + EditIndexerHandler::with(self.key, self.app, self.active_radarr_block, self.context) + .handle() + } _ if IndexerSettingsHandler::accepts(self.active_radarr_block) => { IndexerSettingsHandler::with(self.key, self.app, self.active_radarr_block, self.context) .handle() @@ -41,7 +48,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, } fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { - IndexerSettingsHandler::accepts(active_block) || INDEXERS_BLOCKS.contains(active_block) + EditIndexerHandler::accepts(active_block) + || IndexerSettingsHandler::accepts(active_block) + || TestAllIndexersHandler::accepts(active_block) + || INDEXERS_BLOCKS.contains(active_block) } fn with( @@ -115,7 +125,22 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for IndexersHandler<'a, ActiveRadarrBlock::Indexers => { self .app - .push_navigation_stack(ActiveRadarrBlock::EditIndexer.into()); + .push_navigation_stack(ActiveRadarrBlock::EditIndexerPrompt.into()); + self.app.data.radarr_data.edit_indexer_modal = Some((&self.app.data.radarr_data).into()); + let protocol = &self + .app + .data + .radarr_data + .indexers + .current_selection() + .protocol; + if protocol == "torrent" { + self.app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_TORRENT_SELECTION_BLOCKS); + } else { + self.app.data.radarr_data.selected_block = + BlockSelectionState::new(&EDIT_INDEXER_NZB_SELECTION_BLOCKS); + } } _ => (), } diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 06ef75e..1465494 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -164,30 +164,13 @@ pub struct Indexer { pub priority: i64, #[serde(deserialize_with = "super::from_i64")] pub download_client_id: i64, - pub tags: Option>, + pub tags: Vec, } #[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct IndexerField { - #[serde(deserialize_with = "super::from_i64")] - pub order: i64, pub name: Option, - pub label: Option, pub value: Option, - #[serde(rename(deserialize = "type"))] - pub field_type: Option, - pub select_options: Option>, -} - -#[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, - #[serde(deserialize_with = "super::from_i64")] - pub order: i64, } #[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] diff --git a/src/models/servarr_data/radarr/modals.rs b/src/models/servarr_data/radarr/modals.rs index 9321c42..a093d84 100644 --- a/src/models/servarr_data/radarr/modals.rs +++ b/src/models/servarr_data/radarr/modals.rs @@ -1,6 +1,6 @@ use crate::models::radarr_models::{ - Collection, Credit, MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release, ReleaseField, - RootFolder, + Collection, Credit, Indexer, MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release, + ReleaseField, RootFolder, }; use crate::models::servarr_data::radarr::radarr_data::RadarrData; use crate::models::{HorizontallyScrollableText, ScrollableText, StatefulList, StatefulTable}; @@ -24,6 +24,96 @@ pub struct MovieDetailsModal { pub sort_ascending: Option, } +#[derive(Default, Debug, PartialEq, Eq)] +pub struct EditIndexerModal { + pub name: HorizontallyScrollableText, + pub enable_rss: Option, + pub enable_automatic_search: Option, + pub enable_interactive_search: Option, + 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::>() + .join(", ") + .into(); + + edit_indexer_modal + } +} + #[derive(Default)] pub struct EditMovieModal { pub minimum_availability_list: StatefulList, diff --git a/src/models/servarr_data/radarr/modals_tests.rs b/src/models/servarr_data/radarr/modals_tests.rs index a02ec8e..26861aa 100644 --- a/src/models/servarr_data/radarr/modals_tests.rs +++ b/src/models/servarr_data/radarr/modals_tests.rs @@ -1,8 +1,10 @@ #[cfg(test)] mod test { - use crate::models::radarr_models::{Collection, MinimumAvailability, Monitor, Movie, RootFolder}; + use crate::models::radarr_models::{ + Collection, Indexer, IndexerField, MinimumAvailability, Monitor, Movie, RootFolder, + }; use crate::models::servarr_data::radarr::modals::{ - AddMovieModal, EditCollectionModal, EditMovieModal, + AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, }; use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data; use crate::models::servarr_data::radarr::radarr_data::RadarrData; @@ -10,9 +12,103 @@ mod test { use bimap::BiMap; use pretty_assertions::{assert_eq, assert_str_eq}; use rstest::rstest; - use serde_json::Number; + use serde_json::{Number, Value}; use strum::IntoEnumIterator; + #[rstest] + fn test_edit_indexer_modal_from_radarr_data(#[values(true, false)] seed_ratio_present: bool) { + let mut radarr_data = RadarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..RadarrData::default() + }; + let mut fields = vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://test.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("1234".to_owned())), + }, + ]; + + if seed_ratio_present { + fields.push(IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(Value::from(1.2f64)), + }); + } + + let indexer = Indexer { + name: Some("Test".to_owned()), + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + ..Indexer::default() + }; + radarr_data.indexers.set_items(vec![indexer]); + + let edit_indexer_modal = EditIndexerModal::from(&radarr_data); + + assert_str_eq!(edit_indexer_modal.name.text, "Test"); + assert_eq!(edit_indexer_modal.enable_rss, Some(true)); + assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true)); + assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true)); + assert_str_eq!(edit_indexer_modal.url.text, "https://test.com"); + assert_str_eq!(edit_indexer_modal.api_key.text, "1234"); + + if seed_ratio_present { + assert_str_eq!(edit_indexer_modal.seed_ratio.text, "1.2"); + } else { + assert!(edit_indexer_modal.seed_ratio.text.is_empty()); + } + } + + #[test] + fn test_edit_indexer_modal_from_radarr_data_seed_ratio_value_is_none() { + let mut radarr_data = RadarrData { + tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]), + ..RadarrData::default() + }; + let fields = vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(Value::String("https://test.com".to_owned())), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(Value::String("1234".to_owned())), + }, + IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: None, + }, + ]; + + let indexer = Indexer { + name: Some("Test".to_owned()), + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + tags: vec![Number::from(1), Number::from(2)], + fields: Some(fields), + ..Indexer::default() + }; + radarr_data.indexers.set_items(vec![indexer]); + + let edit_indexer_modal = EditIndexerModal::from(&radarr_data); + + assert_str_eq!(edit_indexer_modal.name.text, "Test"); + assert_eq!(edit_indexer_modal.enable_rss, Some(true)); + assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true)); + assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true)); + assert_str_eq!(edit_indexer_modal.url.text, "https://test.com"); + assert_str_eq!(edit_indexer_modal.api_key.text, "1234"); + assert!(edit_indexer_modal.seed_ratio.text.is_empty()); + } + #[rstest] fn test_edit_movie_modal_from_radarr_data(#[values(true, false)] test_filtered_movies: bool) { let mut radarr_data = RadarrData { diff --git a/src/models/servarr_data/radarr/radarr_data.rs b/src/models/servarr_data/radarr/radarr_data.rs index 64e97d8..f33f56b 100644 --- a/src/models/servarr_data/radarr/radarr_data.rs +++ b/src/models/servarr_data/radarr/radarr_data.rs @@ -10,7 +10,8 @@ use crate::models::radarr_models::{ IndexerSettings, Movie, QueueEvent, RootFolder, Task, }; use crate::models::servarr_data::radarr::modals::{ - AddMovieModal, EditCollectionModal, EditMovieModal, IndexerTestResultModalItem, MovieDetailsModal, + AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem, + MovieDetailsModal, }; use crate::models::{ BlockSelectionState, HorizontallyScrollableText, Route, ScrollableText, StatefulList, @@ -55,6 +56,7 @@ pub struct RadarrData<'a> { pub add_searched_movies: Option>, pub edit_movie_modal: Option, pub edit_collection_modal: Option, + pub edit_indexer_modal: Option, pub edit_root_folder: Option, pub filtered_collections: Option>, pub filtered_movies: Option>, @@ -123,6 +125,7 @@ impl<'a> Default for RadarrData<'a> { add_searched_movies: None, edit_movie_modal: None, edit_collection_modal: None, + edit_indexer_modal: None, edit_root_folder: None, filtered_collections: None, filtered_movies: None, @@ -252,7 +255,16 @@ pub enum ActiveRadarrBlock { EditCollectionSelectQualityProfile, EditCollectionToggleSearchOnAdd, EditCollectionToggleMonitored, - EditIndexer, + EditIndexerPrompt, + EditIndexerConfirmPrompt, + EditIndexerApiKeyInput, + EditIndexerNameInput, + EditIndexerSeedRatioInput, + EditIndexerToggleEnableRss, + EditIndexerToggleEnableAutomaticSearch, + EditIndexerToggleEnableInteractiveSearch, + EditIndexerUrlInput, + EditIndexerTagsInput, EditMoviePrompt, EditMovieConfirmPrompt, EditMoviePathInput, @@ -318,12 +330,10 @@ pub static COLLECTIONS_BLOCKS: [ActiveRadarrBlock; 6] = [ ActiveRadarrBlock::FilterCollectionsError, ActiveRadarrBlock::UpdateAllCollectionsPrompt, ]; -pub static INDEXERS_BLOCKS: [ActiveRadarrBlock; 5] = [ +pub static INDEXERS_BLOCKS: [ActiveRadarrBlock; 3] = [ ActiveRadarrBlock::AddIndexer, - ActiveRadarrBlock::EditIndexer, ActiveRadarrBlock::DeleteIndexerPrompt, ActiveRadarrBlock::Indexers, - ActiveRadarrBlock::TestAllIndexers, ]; pub static ROOT_FOLDERS_BLOCKS: [ActiveRadarrBlock; 3] = [ ActiveRadarrBlock::RootFolders, @@ -416,6 +426,42 @@ pub static DELETE_MOVIE_SELECTION_BLOCKS: [ActiveRadarrBlock; 3] = [ ActiveRadarrBlock::DeleteMovieToggleAddListExclusion, ActiveRadarrBlock::DeleteMovieConfirmPrompt, ]; +pub static EDIT_INDEXER_BLOCKS: [ActiveRadarrBlock; 10] = [ + ActiveRadarrBlock::EditIndexerPrompt, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ActiveRadarrBlock::EditIndexerApiKeyInput, + ActiveRadarrBlock::EditIndexerNameInput, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + ActiveRadarrBlock::EditIndexerToggleEnableRss, + ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveRadarrBlock::EditIndexerUrlInput, + ActiveRadarrBlock::EditIndexerTagsInput, +]; +pub static EDIT_INDEXER_TORRENT_SELECTION_BLOCKS: [ActiveRadarrBlock; 10] = [ + ActiveRadarrBlock::EditIndexerNameInput, + ActiveRadarrBlock::EditIndexerToggleEnableRss, + ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ActiveRadarrBlock::EditIndexerUrlInput, + ActiveRadarrBlock::EditIndexerApiKeyInput, + ActiveRadarrBlock::EditIndexerSeedRatioInput, + ActiveRadarrBlock::EditIndexerTagsInput, + ActiveRadarrBlock::EditIndexerConfirmPrompt, +]; +pub static EDIT_INDEXER_NZB_SELECTION_BLOCKS: [ActiveRadarrBlock; 10] = [ + ActiveRadarrBlock::EditIndexerNameInput, + ActiveRadarrBlock::EditIndexerToggleEnableRss, + ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, + ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ActiveRadarrBlock::EditIndexerUrlInput, + ActiveRadarrBlock::EditIndexerApiKeyInput, + ActiveRadarrBlock::EditIndexerTagsInput, + ActiveRadarrBlock::EditIndexerConfirmPrompt, + ActiveRadarrBlock::EditIndexerConfirmPrompt, +]; pub static INDEXER_SETTINGS_BLOCKS: [ActiveRadarrBlock; 10] = [ ActiveRadarrBlock::IndexerSettingsPrompt, ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, diff --git a/src/models/servarr_data/radarr/radarr_data_tests.rs b/src/models/servarr_data/radarr/radarr_data_tests.rs index f568136..5269296 100644 --- a/src/models/servarr_data/radarr/radarr_data_tests.rs +++ b/src/models/servarr_data/radarr/radarr_data_tests.rs @@ -98,6 +98,7 @@ mod tests { assert!(radarr_data.edit_movie_modal.is_none()); assert!(radarr_data.edit_collection_modal.is_none()); assert!(radarr_data.edit_root_folder.is_none()); + assert!(radarr_data.edit_indexer_modal.is_none()); assert!(radarr_data.filtered_collections.is_none()); assert!(radarr_data.filtered_movies.is_none()); assert!(radarr_data.indexer_settings.is_none()); @@ -270,9 +271,10 @@ mod tests { ActiveRadarrBlock, ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, COLLECTIONS_BLOCKS, COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS, DELETE_MOVIE_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, - EDIT_MOVIE_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, - INDEXER_SETTINGS_SELECTION_BLOCKS, LIBRARY_BLOCKS, MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, - SYSTEM_DETAILS_BLOCKS, + EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS, + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, EDIT_MOVIE_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS, + INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, LIBRARY_BLOCKS, + MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS, }; #[test] @@ -299,12 +301,10 @@ mod tests { #[test] fn test_indexers_blocks_contents() { - assert_eq!(INDEXERS_BLOCKS.len(), 5); + assert_eq!(INDEXERS_BLOCKS.len(), 3); assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::AddIndexer)); - assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::EditIndexer)); assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::DeleteIndexerPrompt)); assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::Indexers)); - assert!(INDEXERS_BLOCKS.contains(&ActiveRadarrBlock::TestAllIndexers)); } #[test] @@ -398,6 +398,25 @@ mod tests { assert!(DELETE_MOVIE_BLOCKS.contains(&ActiveRadarrBlock::DeleteMovieToggleAddListExclusion)); } + #[test] + fn test_edit_indexer_blocks_contents() { + assert_eq!(EDIT_INDEXER_BLOCKS.len(), 10); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerPrompt)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerConfirmPrompt)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerApiKeyInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerNameInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerSeedRatioInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerToggleEnableRss)); + assert!( + EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch) + ); + assert!( + EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch) + ); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerUrlInput)); + assert!(EDIT_INDEXER_BLOCKS.contains(&ActiveRadarrBlock::EditIndexerTagsInput)); + } + #[test] fn test_indexer_settings_blocks_contents() { assert_eq!(INDEXER_SETTINGS_BLOCKS.len(), 10); @@ -542,6 +561,101 @@ mod tests { assert_eq!(delete_movie_block_iter.next(), None); } + #[test] + fn test_edit_indexer_torrent_selection_blocks_ordering() { + let mut edit_indexer_torrent_selection_block_iter = + EDIT_INDEXER_TORRENT_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerNameInput + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerToggleEnableRss + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerConfirmPrompt + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerUrlInput + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerApiKeyInput + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerSeedRatioInput + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerTagsInput + ); + assert_eq!( + edit_indexer_torrent_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerConfirmPrompt + ); + assert_eq!(edit_indexer_torrent_selection_block_iter.next(), None); + } + + #[test] + fn test_edit_indexer_nzb_selection_blocks_ordering() { + let mut edit_indexer_nzb_selection_block_iter = EDIT_INDEXER_NZB_SELECTION_BLOCKS.iter(); + + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerNameInput + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerToggleEnableRss + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerConfirmPrompt + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerUrlInput + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerApiKeyInput + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerTagsInput + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerConfirmPrompt + ); + assert_eq!( + edit_indexer_nzb_selection_block_iter.next().unwrap(), + &ActiveRadarrBlock::EditIndexerConfirmPrompt + ); + assert_eq!(edit_indexer_nzb_selection_block_iter.next(), None); + } + #[test] fn test_indexer_settings_selection_blocks_ordering() { let mut indexer_settings_block_iter = INDEXER_SETTINGS_SELECTION_BLOCKS.iter(); diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 26c0786..9cef674 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -16,7 +16,8 @@ use crate::models::radarr_models::{ Update, }; use crate::models::servarr_data::radarr::modals::{ - AddMovieModal, EditCollectionModal, EditMovieModal, IndexerTestResultModalItem, MovieDetailsModal, + AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem, + MovieDetailsModal, }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText, StatefulTable}; @@ -36,7 +37,9 @@ pub enum RadarrEvent { DeleteMovie, DeleteRootFolder, DownloadRelease, + EditAllIndexerSettings, EditCollection, + EditIndexer, EditMovie, GetCollections, GetDownloads, @@ -65,7 +68,6 @@ pub enum RadarrEvent { UpdateAndScan, UpdateCollections, UpdateDownloads, - UpdateIndexerSettings, } impl RadarrEvent { @@ -73,8 +75,10 @@ impl RadarrEvent { match self { RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection", RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue", - RadarrEvent::GetIndexers | RadarrEvent::DeleteIndexer => "/indexer", - RadarrEvent::GetIndexerSettings | RadarrEvent::UpdateIndexerSettings => "/config/indexer", + RadarrEvent::GetIndexers | RadarrEvent::EditIndexer | RadarrEvent::DeleteIndexer => { + "/indexer" + } + RadarrEvent::GetIndexerSettings | RadarrEvent::EditAllIndexerSettings => "/config/indexer", RadarrEvent::GetLogs => "/log", RadarrEvent::AddMovie | RadarrEvent::EditMovie @@ -123,7 +127,9 @@ impl<'a, 'b> Network<'a, 'b> { RadarrEvent::DeleteMovie => self.delete_movie().await, RadarrEvent::DeleteRootFolder => self.delete_root_folder().await, RadarrEvent::DownloadRelease => self.download_release().await, + RadarrEvent::EditAllIndexerSettings => self.edit_all_indexer_settings().await, RadarrEvent::EditCollection => self.edit_collection().await, + RadarrEvent::EditIndexer => self.edit_indexer().await, RadarrEvent::EditMovie => self.edit_movie().await, RadarrEvent::GetCollections => self.get_collections().await, RadarrEvent::GetDownloads => self.get_downloads().await, @@ -152,7 +158,6 @@ impl<'a, 'b> Network<'a, 'b> { RadarrEvent::UpdateAndScan => self.update_and_scan().await, RadarrEvent::UpdateCollections => self.update_collections().await, RadarrEvent::UpdateDownloads => self.update_downloads().await, - RadarrEvent::UpdateIndexerSettings => self.update_indexer_settings().await, } } @@ -466,6 +471,37 @@ impl<'a, 'b> Network<'a, 'b> { .await; } + async fn edit_all_indexer_settings(&mut self) { + info!("Updating Radarr indexer settings"); + + let body = self + .app + .lock() + .await + .data + .radarr_data + .indexer_settings + .as_ref() + .unwrap() + .clone(); + + debug!("Indexer settings body: {body:?}"); + + let request_props = self + .radarr_request_props_from( + RadarrEvent::EditAllIndexerSettings.resource(), + RequestMethod::Put, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| {}) + .await; + + self.app.lock().await.data.radarr_data.indexer_settings = None; + } + async fn edit_collection(&mut self) { info!("Editing Radarr collection"); @@ -545,6 +581,129 @@ impl<'a, 'b> Network<'a, 'b> { .await; } + async fn edit_indexer(&mut self) { + let id = self + .app + .lock() + .await + .data + .radarr_data + .indexers + .current_selection() + .id; + info!("Updating Radarr indexer with ID: {id}"); + + info!("Fetching indexer details for indexer with ID: {id}"); + + let request_props = self + .radarr_request_props_from( + format!("{}/{id}", RadarrEvent::GetIndexers.resource()).as_str(), + RequestMethod::Get, + None::<()>, + ) + .await; + + let mut response = String::new(); + + self + .handle_request::<(), Value>(request_props, |detailed_indexer_body, _| { + response = detailed_indexer_body.to_string() + }) + .await; + + info!("Constructing edit indexer body"); + + let body = { + let tags = self + .app + .lock() + .await + .data + .radarr_data + .edit_indexer_modal + .as_ref() + .unwrap() + .tags + .text + .clone(); + let tag_ids_vec = self.extract_and_add_tag_ids_vec(tags).await; + let mut app = self.app.lock().await; + let mut detailed_indexer_body: Value = serde_json::from_str(&response).unwrap(); + + let EditIndexerModal { + name, + enable_rss, + enable_automatic_search, + enable_interactive_search, + url, + api_key, + seed_ratio, + .. + } = app.data.radarr_data.edit_indexer_modal.as_ref().unwrap(); + + *detailed_indexer_body.get_mut("name").unwrap() = json!(name.text.clone()); + *detailed_indexer_body.get_mut("enableRss").unwrap() = json!(enable_rss.unwrap_or_default()); + *detailed_indexer_body + .get_mut("enableAutomaticSearch") + .unwrap() = json!(enable_automatic_search.unwrap_or_default()); + *detailed_indexer_body + .get_mut("enableInteractiveSearch") + .unwrap() = json!(enable_interactive_search.unwrap_or_default()); + *detailed_indexer_body + .get_mut("fields") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|field| field["name"] == "baseUrl") + .unwrap() + .get_mut("value") + .unwrap() = json!(url.text.clone()); + *detailed_indexer_body + .get_mut("fields") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|field| field["name"] == "apiKey") + .unwrap() + .get_mut("value") + .unwrap() = json!(api_key.text.clone()); + *detailed_indexer_body.get_mut("tags").unwrap() = json!(tag_ids_vec); + let seed_ratio_field_option = detailed_indexer_body + .get_mut("fields") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .find(|field| field["name"] == "seedCriteria.seedRatio"); + if let Some(seed_ratio_field) = seed_ratio_field_option { + seed_ratio_field + .as_object_mut() + .unwrap() + .insert("value".to_string(), json!(seed_ratio.text.clone())); + } + + app.data.radarr_data.edit_indexer_modal = None; + + detailed_indexer_body + }; + + debug!("Edit indexer body: {body:?}"); + + let request_props = self + .radarr_request_props_from( + format!("{}/{id}", RadarrEvent::EditIndexer.resource()).as_str(), + RequestMethod::Put, + Some(body), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + async fn edit_movie(&mut self) { info!("Editing Radarr movie"); @@ -663,13 +822,13 @@ impl<'a, 'b> Network<'a, 'b> { .handle_request::<(), Vec>(request_props, |credit_vec, mut app| { let cast_vec: Vec = credit_vec .iter() + .filter(|&credit| credit.credit_type == CreditType::Cast) .cloned() - .filter(|credit| credit.credit_type == CreditType::Cast) .collect(); let crew_vec: Vec = credit_vec .iter() + .filter(|&credit| credit.credit_type == CreditType::Crew) .cloned() - .filter(|credit| credit.credit_type == CreditType::Crew) .collect(); if app.data.radarr_data.movie_details_modal.is_none() { @@ -1500,37 +1659,6 @@ impl<'a, 'b> Network<'a, 'b> { .await; } - async fn update_indexer_settings(&mut self) { - info!("Updating Radarr indexer settings"); - - let body = self - .app - .lock() - .await - .data - .radarr_data - .indexer_settings - .as_ref() - .unwrap() - .clone(); - - debug!("Indexer settings body: {body:?}"); - - let request_props = self - .radarr_request_props_from( - RadarrEvent::UpdateIndexerSettings.resource(), - RequestMethod::Put, - Some(body), - ) - .await; - - self - .handle_request::(request_props, |_, _| {}) - .await; - - self.app.lock().await.data.radarr_data.indexer_settings = None; - } - async fn radarr_request_props_from( &self, resource: &str, diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 193f423..91c955c 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -13,8 +13,8 @@ mod test { use tokio_util::sync::CancellationToken; use crate::models::radarr_models::{ - CollectionMovie, IndexerField, IndexerSelectOption, Language, MediaInfo, MinimumAvailability, - Monitor, MovieFile, Quality, QualityWrapper, Rating, RatingsList, + CollectionMovie, IndexerField, Language, MediaInfo, MinimumAvailability, Monitor, MovieFile, + Quality, QualityWrapper, Rating, RatingsList, }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::{HorizontallyScrollableText, StatefulTable}; @@ -136,7 +136,7 @@ mod test { #[rstest] fn test_resource_indexer_settings( - #[values(RadarrEvent::GetIndexerSettings, RadarrEvent::UpdateIndexerSettings)] + #[values(RadarrEvent::GetIndexerSettings, RadarrEvent::EditAllIndexerSettings)] event: RadarrEvent, ) { assert_str_eq!(event.resource(), "/config/indexer"); @@ -782,45 +782,6 @@ mod test { async_server.assert_async().await; } - #[tokio::test] - async fn test_handle_update_indexer_settings_event() { - let indexer_settings_json = json!({ - "minimumAge": 0, - "maximumSize": 0, - "retention": 0, - "rssSyncInterval": 60, - "preferIndexerFlags": false, - "availabilityDelay": 0, - "allowHardcodedSubs": true, - "whitelistedHardcodedSubs": "", - "id": 1 - }); - let (async_server, app_arc, _server) = mock_radarr_api( - RequestMethod::Put, - Some(indexer_settings_json), - None, - None, - RadarrEvent::UpdateIndexerSettings.resource(), - ) - .await; - - app_arc.lock().await.data.radarr_data.indexer_settings = Some(indexer_settings()); - let mut network = Network::new(&app_arc, CancellationToken::new()); - - network - .handle_radarr_event(RadarrEvent::UpdateIndexerSettings) - .await; - - async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .radarr_data - .indexer_settings - .is_none()); - } - #[tokio::test] async fn test_handle_update_collections_event() { let (async_server, app_arc, _server) = mock_radarr_api( @@ -1211,37 +1172,22 @@ mod test { "name": "Test Indexer", "fields": [ { - "order": 0, - "name": "valueIsString", - "label": "Value Is String", - "value": "hello", - "type": "textbox", + "name": "baseUrl", + "value": "https://test.com", }, { - "order": 1, - "name": "emptyValueWithSelectOptions", - "label": "Empty Value With Select Options", - "type": "select", - "selectOptions": [ - { - "value": -2, - "name": "Original", - "order": 0, - } - ] + "name": "apiKey", + "value": "", }, { - "order": 2, - "name": "valueIsAnArray", - "label": "Value is an array", - "value": [1, 2], - "type": "select", + "name": "seedCriteria.seedRatio", + "value": "1.2", }, ], "implementationName": "Torznab", "implementation": "Torznab", "configContract": "TorznabSettings", - "tags": ["test_tag"], + "tags": [1], "id": 1 }]); let (async_server, app_arc, _server) = mock_radarr_api( @@ -2083,66 +2029,42 @@ mod test { } #[tokio::test] - async fn test_handle_edit_movie_event() { - let mut expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); - *expected_body.get_mut("monitored").unwrap() = json!(false); - *expected_body.get_mut("minimumAvailability").unwrap() = json!("announced"); - *expected_body.get_mut("qualityProfileId").unwrap() = json!(1111); - *expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); - *expected_body.get_mut("tags").unwrap() = json!([1, 2]); - - let resource = format!("{}/1", RadarrEvent::GetMovieDetails.resource()); - let (async_details_server, app_arc, mut server) = mock_radarr_api( - RequestMethod::Get, + async fn test_handle_edit_all_indexer_settings_event() { + let indexer_settings_json = json!({ + "minimumAge": 0, + "maximumSize": 0, + "retention": 0, + "rssSyncInterval": 60, + "preferIndexerFlags": false, + "availabilityDelay": 0, + "allowHardcodedSubs": true, + "whitelistedHardcodedSubs": "", + "id": 1 + }); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Put, + Some(indexer_settings_json), None, - Some(serde_json::from_str(MOVIE_JSON).unwrap()), None, - &resource, + RadarrEvent::EditAllIndexerSettings.resource(), ) .await; - let async_edit_server = server - .mock( - "PUT", - format!("/api/v3{}/1", RadarrEvent::EditMovie.resource()).as_str(), - ) - .with_status(202) - .match_header("X-Api-Key", "test1234") - .match_body(Matcher::Json(expected_body)) - .create_async() - .await; - { - let mut app = app_arc.lock().await; - app.data.radarr_data.tags_map = - BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); - let mut edit_movie = EditMovieModal { - tags: "usenet, testing".to_owned().into(), - path: "/nfs/Test Path".to_owned().into(), - monitored: Some(false), - ..EditMovieModal::default() - }; - edit_movie - .quality_profile_list - .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); - edit_movie - .minimum_availability_list - .set_items(Vec::from_iter(MinimumAvailability::iter())); - app.data.radarr_data.edit_movie_modal = Some(edit_movie); - app.data.radarr_data.movies.set_items(vec![Movie { - monitored: false, - ..movie() - }]); - app.data.radarr_data.quality_profile_map = - BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); - } + + app_arc.lock().await.data.radarr_data.indexer_settings = Some(indexer_settings()); let mut network = Network::new(&app_arc, CancellationToken::new()); - network.handle_radarr_event(RadarrEvent::EditMovie).await; + network + .handle_radarr_event(RadarrEvent::EditAllIndexerSettings) + .await; - async_details_server.assert_async().await; - async_edit_server.assert_async().await; - - let app = app_arc.lock().await; - assert!(app.data.radarr_data.edit_movie_modal.is_none()); + async_server.assert_async().await; + assert!(app_arc + .lock() + .await + .data + .radarr_data + .indexer_settings + .is_none()); } #[tokio::test] @@ -2240,6 +2162,369 @@ mod test { assert!(app.data.radarr_data.edit_collection_modal.is_none()); } + #[tokio::test] + async fn test_handle_edit_indexer_event() { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.2", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.3", + }, + ], + "tags": [1, 2], + "id": 1 + }); + + let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(indexer_details_json), + None, + &resource, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1", RadarrEvent::EditIndexer.resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + { + let mut app = app_arc.lock().await; + app.data.radarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + }; + app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal); + app.data.radarr_data.indexers.set_items(vec![indexer()]); + } + let mut network = Network::new(&app_arc, CancellationToken::new()); + + network.handle_radarr_event(RadarrEvent::EditIndexer).await; + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + + let app = app_arc.lock().await; + assert!(app.data.radarr_data.edit_indexer_modal.is_none()); + } + + #[tokio::test] + async fn test_handle_edit_indexer_event_does_not_add_seed_ratio_when_seed_ratio_field_is_none_in_details( + ) { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + ], + "tags": [1, 2], + "id": 1 + }); + + let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(indexer_details_json), + None, + &resource, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1", RadarrEvent::EditIndexer.resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + { + let mut app = app_arc.lock().await; + app.data.radarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + }; + app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal); + let mut indexer = indexer(); + indexer.fields = Some( + indexer + .fields + .unwrap() + .into_iter() + .filter(|field| field.name != Some("seedCriteria.seedRatio".to_string())) + .collect(), + ); + app.data.radarr_data.indexers.set_items(vec![indexer]); + } + let mut network = Network::new(&app_arc, CancellationToken::new()); + + network.handle_radarr_event(RadarrEvent::EditIndexer).await; + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + + let app = app_arc.lock().await; + assert!(app.data.radarr_data.edit_indexer_modal.is_none()); + } + + #[tokio::test] + async fn test_handle_edit_indexer_event_populates_the_seed_ratio_value_when_seed_ratio_field_is_present_in_details( + ) { + let indexer_details_json = json!({ + "enableRss": true, + "enableAutomaticSearch": true, + "enableInteractiveSearch": true, + "name": "Test Indexer", + "fields": [ + { + "name": "baseUrl", + "value": "https://test.com", + }, + { + "name": "apiKey", + "value": "", + }, + { + "name": "seedCriteria.seedRatio", + }, + ], + "tags": [1], + "id": 1 + }); + let expected_indexer_edit_body_json = json!({ + "enableRss": false, + "enableAutomaticSearch": false, + "enableInteractiveSearch": false, + "name": "Test Update", + "fields": [ + { + "name": "baseUrl", + "value": "https://localhost:9696/1/", + }, + { + "name": "apiKey", + "value": "test1234", + }, + { + "name": "seedCriteria.seedRatio", + "value": "1.3", + }, + ], + "tags": [1, 2], + "id": 1 + }); + + let resource = format!("{}/1", RadarrEvent::GetIndexers.resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(indexer_details_json), + None, + &resource, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1", RadarrEvent::EditIndexer.resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_indexer_edit_body_json)) + .create_async() + .await; + { + let mut app = app_arc.lock().await; + app.data.radarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + let edit_indexer_modal = EditIndexerModal { + name: "Test Update".into(), + enable_rss: Some(false), + enable_automatic_search: Some(false), + enable_interactive_search: Some(false), + url: "https://localhost:9696/1/".into(), + api_key: "test1234".into(), + seed_ratio: "1.3".into(), + tags: "usenet, testing".into(), + }; + app.data.radarr_data.edit_indexer_modal = Some(edit_indexer_modal); + let mut indexer = indexer(); + indexer.fields = Some( + indexer + .fields + .unwrap() + .into_iter() + .map(|mut field| { + if field.name == Some("seedCriteria.seedRatio".to_string()) { + field.value = None; + field + } else { + field + } + }) + .collect(), + ); + app.data.radarr_data.indexers.set_items(vec![indexer]); + } + let mut network = Network::new(&app_arc, CancellationToken::new()); + + network.handle_radarr_event(RadarrEvent::EditIndexer).await; + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + + let app = app_arc.lock().await; + assert!(app.data.radarr_data.edit_indexer_modal.is_none()); + } + + #[tokio::test] + async fn test_handle_edit_movie_event() { + let mut expected_body: Value = serde_json::from_str(MOVIE_JSON).unwrap(); + *expected_body.get_mut("monitored").unwrap() = json!(false); + *expected_body.get_mut("minimumAvailability").unwrap() = json!("announced"); + *expected_body.get_mut("qualityProfileId").unwrap() = json!(1111); + *expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path"); + *expected_body.get_mut("tags").unwrap() = json!([1, 2]); + + let resource = format!("{}/1", RadarrEvent::GetMovieDetails.resource()); + let (async_details_server, app_arc, mut server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(serde_json::from_str(MOVIE_JSON).unwrap()), + None, + &resource, + ) + .await; + let async_edit_server = server + .mock( + "PUT", + format!("/api/v3{}/1", RadarrEvent::EditMovie.resource()).as_str(), + ) + .with_status(202) + .match_header("X-Api-Key", "test1234") + .match_body(Matcher::Json(expected_body)) + .create_async() + .await; + { + let mut app = app_arc.lock().await; + app.data.radarr_data.tags_map = + BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]); + let mut edit_movie = EditMovieModal { + tags: "usenet, testing".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + monitored: Some(false), + ..EditMovieModal::default() + }; + edit_movie + .quality_profile_list + .set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]); + edit_movie + .minimum_availability_list + .set_items(Vec::from_iter(MinimumAvailability::iter())); + app.data.radarr_data.edit_movie_modal = Some(edit_movie); + app.data.radarr_data.movies.set_items(vec![Movie { + monitored: false, + ..movie() + }]); + app.data.radarr_data.quality_profile_map = + BiMap::from_iter([(1111, "Any".to_owned()), (2222, "HD - 1080p".to_owned())]); + } + let mut network = Network::new(&app_arc, CancellationToken::new()); + + network.handle_radarr_event(RadarrEvent::EditMovie).await; + + async_details_server.assert_async().await; + async_edit_server.assert_async().await; + + let app = app_arc.lock().await; + assert!(app.data.radarr_data.edit_movie_modal.is_none()); + } + #[tokio::test] async fn test_handle_download_release_event() { let (async_server, app_arc, _server) = mock_radarr_api( @@ -2786,36 +3071,20 @@ mod test { implementation_name: Some("Torznab".to_owned()), implementation: Some("Torznab".to_owned()), config_contract: Some("TorznabSettings".to_owned()), - tags: Some(vec!["test_tag".to_owned()]), + tags: vec![Number::from(1)], id: 1, fields: Some(vec![ IndexerField { - order: 0, - name: Some("valueIsString".to_owned()), - label: Some("Value Is String".to_owned()), - value: Some(json!("hello")), - field_type: Some("textbox".to_owned()), - select_options: None, + name: Some("baseUrl".to_owned()), + value: Some(json!("https://test.com")), }, IndexerField { - order: 1, - name: Some("emptyValueWithSelectOptions".to_owned()), - label: Some("Empty Value With Select Options".to_owned()), - value: None, - field_type: Some("select".to_owned()), - select_options: Some(vec![IndexerSelectOption { - value: -2, - name: Some("Original".to_owned()), - order: 0, - }]), + name: Some("apiKey".to_owned()), + value: Some(json!("")), }, IndexerField { - order: 2, - name: Some("valueIsAnArray".to_owned()), - label: Some("Value is an array".to_owned()), - value: Some(json!([1, 2])), - field_type: Some("select".to_owned()), - select_options: None, + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(json!("1.2")), }, ]), } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 5ec762a..1c8965e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -676,15 +676,26 @@ fn draw_help_and_get_content_rect(f: &mut Frame<'_>, area: Rect, help: Option, - text_box_area: Rect, - block_title: Option<&str>, - block_content: &str, - offset: usize, - should_show_cursor: bool, - is_selected: bool, -) { +pub struct TextBoxProps<'a> { + pub text_box_area: Rect, + pub block_title: Option<&'a str>, + pub block_content: &'a str, + pub offset: usize, + pub should_show_cursor: bool, + pub is_selected: bool, + pub cursor_after_string: bool, +} + +pub fn draw_text_box(f: &mut Frame<'_>, text_box_props: TextBoxProps<'_>) { + let TextBoxProps { + text_box_area, + block_title, + block_content, + offset, + should_show_cursor, + is_selected, + cursor_after_string, + } = text_box_props; let (block, style) = if let Some(title) = block_title { (title_block_centered(title), style_default()) } else { @@ -703,19 +714,33 @@ pub fn draw_text_box( f.render_widget(paragraph, text_box_area); if should_show_cursor { - show_cursor(f, text_box_area, offset, block_content); + show_cursor(f, text_box_area, offset, block_content, cursor_after_string); } } +pub struct LabeledTextBoxProps<'a> { + pub area: Rect, + pub label: &'a str, + pub text: &'a str, + pub offset: usize, + pub is_selected: bool, + pub should_show_cursor: bool, + pub cursor_after_string: bool, +} + pub fn draw_text_box_with_label( f: &mut Frame<'_>, - area: Rect, - label: &str, - text: &str, - offset: usize, - is_selected: bool, - should_show_cursor: bool, + labeled_text_box_props: LabeledTextBoxProps<'_>, ) { + let LabeledTextBoxProps { + area, + label, + text, + offset, + is_selected, + should_show_cursor, + cursor_after_string, + } = labeled_text_box_props; let horizontal_chunks = horizontal_chunks( vec![ Constraint::Percentage(48), @@ -734,12 +759,15 @@ pub fn draw_text_box_with_label( draw_text_box( f, - horizontal_chunks[1], - None, - text, - offset, - should_show_cursor, - is_selected, + TextBoxProps { + text_box_area: horizontal_chunks[1], + block_title: None, + block_content: text, + offset, + should_show_cursor, + is_selected, + cursor_after_string, + }, ); } @@ -761,12 +789,15 @@ pub fn draw_input_box_popup( draw_text_box( f, - chunks[0], - Some(box_title), - &box_content.text, - *box_content.offset.borrow(), - true, - false, + TextBoxProps { + text_box_area: chunks[0], + block_title: Some(box_title), + block_content: &box_content.text, + offset: *box_content.offset.borrow(), + should_show_cursor: true, + is_selected: false, + cursor_after_string: true, + }, ); let help = Paragraph::new(" cancel") diff --git a/src/ui/radarr_ui/collections/edit_collection_ui.rs b/src/ui/radarr_ui/collections/edit_collection_ui.rs index f7db01f..56adfcb 100644 --- a/src/ui/radarr_ui/collections/edit_collection_ui.rs +++ b/src/ui/radarr_ui/collections/edit_collection_ui.rs @@ -16,7 +16,7 @@ use crate::ui::utils::{ use crate::ui::{ draw_button, draw_checkbox_with_label, draw_drop_down_menu_button, draw_drop_down_popup, draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over, draw_popup, - draw_selectable_list, draw_text_box_with_label, DrawUi, + draw_selectable_list, draw_text_box_with_label, DrawUi, LabeledTextBoxProps, }; #[cfg(test)] @@ -191,12 +191,16 @@ fn draw_edit_collection_confirmation_prompt( if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { draw_text_box_with_label( f, - chunks[4], - "Root Folder", - &path.text, - *path.offset.borrow(), - selected_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput, - active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput, + LabeledTextBoxProps { + area: chunks[4], + label: "Root Folder", + text: &path.text, + offset: *path.offset.borrow(), + is_selected: selected_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput, + should_show_cursor: active_radarr_block + == ActiveRadarrBlock::EditCollectionRootFolderPathInput, + cursor_after_string: true, + }, ); } diff --git a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs new file mode 100644 index 0000000..460a2ee --- /dev/null +++ b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs @@ -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); + } +} diff --git a/src/ui/radarr_ui/indexers/edit_indexer_ui_tests.rs b/src/ui/radarr_ui/indexers/edit_indexer_ui_tests.rs new file mode 100644 index 0000000..09b1c97 --- /dev/null +++ b/src/ui/radarr_ui/indexers/edit_indexer_ui_tests.rs @@ -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())); + } + }); + } +} diff --git a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs index eb4b4f7..015a069 100644 --- a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs +++ b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs @@ -13,7 +13,8 @@ use crate::ui::utils::{ vertical_chunks_with_margin, }; use crate::ui::{ - draw_button, draw_checkbox_with_label, draw_popup_over, draw_text_box_with_label, loading, DrawUi, + draw_button, draw_checkbox_with_label, draw_popup_over, draw_text_box_with_label, loading, + DrawUi, LabeledTextBoxProps, }; #[cfg(test)] @@ -91,57 +92,82 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, promp if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { draw_text_box_with_label( f, - left_chunks[0], - "Minimum Age (minutes) ▴▾", - &indexer_settings.minimum_age.to_string(), - 0, - selected_block == &ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, - active_radarr_block == ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, + LabeledTextBoxProps { + area: left_chunks[0], + label: "Minimum Age (minutes) ▴▾", + text: &indexer_settings.minimum_age.to_string(), + offset: 0, + is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, + should_show_cursor: active_radarr_block + == ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, + cursor_after_string: false, + }, ); draw_text_box_with_label( f, - left_chunks[1], - "Retention (days) ▴▾", - &indexer_settings.retention.to_string(), - 0, - selected_block == &ActiveRadarrBlock::IndexerSettingsRetentionInput, - active_radarr_block == ActiveRadarrBlock::IndexerSettingsRetentionInput, + LabeledTextBoxProps { + area: left_chunks[1], + label: "Retention (days) ▴▾", + text: &indexer_settings.retention.to_string(), + offset: 0, + is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsRetentionInput, + should_show_cursor: active_radarr_block + == ActiveRadarrBlock::IndexerSettingsRetentionInput, + cursor_after_string: false, + }, ); draw_text_box_with_label( f, - left_chunks[2], - "Maximum Size (MB) ▴▾", - &indexer_settings.maximum_size.to_string(), - 0, - selected_block == &ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, - active_radarr_block == ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, + LabeledTextBoxProps { + area: left_chunks[2], + label: "Maximum Size (MB) ▴▾", + text: &indexer_settings.maximum_size.to_string(), + offset: 0, + is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, + should_show_cursor: active_radarr_block + == ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, + cursor_after_string: false, + }, ); draw_text_box_with_label( f, - right_chunks[0], - "Availability Delay (days) ▴▾", - &indexer_settings.availability_delay.to_string(), - 0, - selected_block == &ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, - active_radarr_block == ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, + LabeledTextBoxProps { + area: right_chunks[0], + label: "Availability Delay (days) ▴▾", + text: &indexer_settings.availability_delay.to_string(), + offset: 0, + is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, + should_show_cursor: active_radarr_block + == ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, + cursor_after_string: false, + }, ); draw_text_box_with_label( f, - right_chunks[1], - "RSS Sync Interval (minutes) ▴▾", - &indexer_settings.rss_sync_interval.to_string(), - 0, - selected_block == &ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, - active_radarr_block == ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, + LabeledTextBoxProps { + area: right_chunks[1], + label: "RSS Sync Interval (minutes) ▴▾", + text: &indexer_settings.rss_sync_interval.to_string(), + offset: 0, + is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, + should_show_cursor: active_radarr_block + == ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, + cursor_after_string: false, + }, ); draw_text_box_with_label( f, - right_chunks[2], - "Whitelisted Subtitle Tags", - &indexer_settings.whitelisted_hardcoded_subs.text, - *indexer_settings.whitelisted_hardcoded_subs.offset.borrow(), - selected_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, - active_radarr_block == ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + LabeledTextBoxProps { + area: right_chunks[2], + label: "Whitelisted Subtitle Tags", + text: &indexer_settings.whitelisted_hardcoded_subs.text, + offset: *indexer_settings.whitelisted_hardcoded_subs.offset.borrow(), + is_selected: selected_block + == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + should_show_cursor: active_radarr_block + == ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + cursor_after_string: true, + }, ); } diff --git a/src/ui/radarr_ui/indexers/indexers_ui_tests.rs b/src/ui/radarr_ui/indexers/indexers_ui_tests.rs index 7b6a135..4bf9eec 100644 --- a/src/ui/radarr_ui/indexers/indexers_ui_tests.rs +++ b/src/ui/radarr_ui/indexers/indexers_ui_tests.rs @@ -3,7 +3,7 @@ mod tests { use strum::IntoEnumIterator; use crate::models::servarr_data::radarr::radarr_data::{ - ActiveRadarrBlock, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, + ActiveRadarrBlock, EDIT_INDEXER_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, }; use crate::ui::radarr_ui::indexers::IndexersUi; use crate::ui::DrawUi; @@ -13,6 +13,8 @@ mod tests { let mut indexers_blocks = Vec::new(); indexers_blocks.extend(INDEXERS_BLOCKS); indexers_blocks.extend(INDEXER_SETTINGS_BLOCKS); + indexers_blocks.extend(EDIT_INDEXER_BLOCKS); + indexers_blocks.push(ActiveRadarrBlock::TestAllIndexers); ActiveRadarrBlock::iter().for_each(|active_radarr_block| { if indexers_blocks.contains(&active_radarr_block) { diff --git a/src/ui/radarr_ui/indexers/mod.rs b/src/ui/radarr_ui/indexers/mod.rs index 58b4d46..1d5c660 100644 --- a/src/ui/radarr_ui/indexers/mod.rs +++ b/src/ui/radarr_ui/indexers/mod.rs @@ -7,24 +7,29 @@ use crate::app::App; use crate::models::radarr_models::Indexer; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, INDEXERS_BLOCKS}; use crate::models::Route; +use crate::ui::radarr_ui::indexers::edit_indexer_ui::EditIndexerUi; use crate::ui::radarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi; use crate::ui::radarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi; use crate::ui::utils::{layout_block_top_border, style_failure, style_primary, style_success}; use crate::ui::{draw_prompt_box, draw_prompt_popup_over, draw_table, DrawUi, TableProps}; +mod edit_indexer_ui; mod indexer_settings_ui; +mod test_all_indexers_ui; #[cfg(test)] #[path = "indexers_ui_tests.rs"] mod indexers_ui_tests; -mod test_all_indexers_ui; pub(super) struct IndexersUi; impl DrawUi for IndexersUi { fn accepts(route: Route) -> bool { if let Route::Radarr(active_radarr_block, _) = route { - return IndexerSettingsUi::accepts(route) || INDEXERS_BLOCKS.contains(&active_radarr_block); + return EditIndexerUi::accepts(route) + || IndexerSettingsUi::accepts(route) + || TestAllIndexersUi::accepts(route) + || INDEXERS_BLOCKS.contains(&active_radarr_block); } false @@ -45,6 +50,7 @@ impl DrawUi for IndexersUi { }; match route { + _ if EditIndexerUi::accepts(route) => EditIndexerUi::draw(f, app, content_rect), _ if IndexerSettingsUi::accepts(route) => IndexerSettingsUi::draw(f, app, content_rect), _ if TestAllIndexersUi::accepts(route) => TestAllIndexersUi::draw(f, app, content_rect), Route::Radarr(active_radarr_block, _) if INDEXERS_BLOCKS.contains(&active_radarr_block) => { @@ -69,13 +75,15 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { "Automatic Search", "Interactive Search", "Priority", + "Tags", ], constraints: vec![ - Constraint::Ratio(1, 5), - Constraint::Ratio(1, 5), - Constraint::Ratio(1, 5), - Constraint::Ratio(1, 5), - Constraint::Ratio(1, 5), + Constraint::Percentage(25), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(13), + Constraint::Percentage(23), ], help: app .data @@ -90,6 +98,7 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { enable_automatic_search, enable_interactive_search, priority, + tags, .. } = indexer; let bool_to_text = |flag: bool| { @@ -112,6 +121,19 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { bool_to_text(*enable_interactive_search); let mut interactive_search = Text::from(interactive_search_text); interactive_search.patch_style(interactive_search_style); + let tags: String = tags + .iter() + .map(|tag_id| { + app + .data + .radarr_data + .tags_map + .get_by_left(&tag_id.as_i64().unwrap()) + .unwrap() + .clone() + }) + .collect::>() + .join(", "); Row::new(vec![ Cell::from(name.clone().unwrap_or_default()), @@ -119,6 +141,7 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Cell::from(automatic_search), Cell::from(interactive_search), Cell::from(priority.to_string()), + Cell::from(tags), ]) .style(style_primary()) }, diff --git a/src/ui/radarr_ui/library/add_movie_ui.rs b/src/ui/radarr_ui/library/add_movie_ui.rs index 9d0bc0b..c239eca 100644 --- a/src/ui/radarr_ui/library/add_movie_ui.rs +++ b/src/ui/radarr_ui/library/add_movie_ui.rs @@ -19,7 +19,8 @@ use crate::ui::utils::{ use crate::ui::{ draw_button, draw_drop_down_menu_button, draw_drop_down_popup, draw_error_popup, draw_error_popup_over, draw_large_popup_over, draw_medium_popup_over, draw_selectable_list, - draw_table, draw_text_box, draw_text_box_with_label, DrawUi, TableProps, + draw_table, draw_text_box, draw_text_box_with_label, DrawUi, LabeledTextBoxProps, TableProps, + TextBoxProps, }; use crate::utils::convert_runtime; use crate::App; @@ -135,12 +136,15 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ActiveRadarrBlock::AddMovieSearchInput => { draw_text_box( f, - chunks[0], - Some("Add Movie"), - block_content, - offset, - true, - false, + TextBoxProps { + text_box_area: chunks[0], + block_title: Some("Add Movie"), + block_content, + offset, + should_show_cursor: true, + is_selected: false, + cursor_after_string: true, + }, ); f.render_widget(layout_block(), chunks[1]); @@ -267,12 +271,15 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { draw_text_box( f, - chunks[0], - Some("Add Movie"), - block_content, - offset, - false, - false, + TextBoxProps { + text_box_area: chunks[0], + block_title: Some("Add Movie"), + block_content, + offset, + should_show_cursor: false, + is_selected: false, + cursor_after_string: true, + }, ); } @@ -441,12 +448,15 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, prompt_area: R if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { draw_text_box_with_label( f, - chunks[5], - "Tags", - &tags.text, - *tags.offset.borrow(), - selected_block == &ActiveRadarrBlock::AddMovieTagsInput, - active_radarr_block == ActiveRadarrBlock::AddMovieTagsInput, + LabeledTextBoxProps { + area: chunks[5], + label: "Tags", + text: &tags.text, + offset: *tags.offset.borrow(), + is_selected: selected_block == &ActiveRadarrBlock::AddMovieTagsInput, + should_show_cursor: active_radarr_block == ActiveRadarrBlock::AddMovieTagsInput, + cursor_after_string: true, + }, ); } diff --git a/src/ui/radarr_ui/library/edit_movie_ui.rs b/src/ui/radarr_ui/library/edit_movie_ui.rs index 66ed4a0..1b5aaab 100644 --- a/src/ui/radarr_ui/library/edit_movie_ui.rs +++ b/src/ui/radarr_ui/library/edit_movie_ui.rs @@ -17,7 +17,7 @@ use crate::ui::utils::{ use crate::ui::{ draw_button, draw_checkbox_with_label, draw_drop_down_menu_button, draw_drop_down_popup, draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over, draw_popup, - draw_selectable_list, draw_text_box_with_label, DrawUi, + draw_selectable_list, draw_text_box_with_label, DrawUi, LabeledTextBoxProps, }; #[cfg(test)] @@ -179,21 +179,27 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, pro if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { draw_text_box_with_label( f, - chunks[4], - "Path", - &path.text, - *path.offset.borrow(), - selected_block == &ActiveRadarrBlock::EditMoviePathInput, - active_radarr_block == ActiveRadarrBlock::EditMoviePathInput, + LabeledTextBoxProps { + area: chunks[4], + label: "Path", + text: &path.text, + offset: *path.offset.borrow(), + is_selected: selected_block == &ActiveRadarrBlock::EditMoviePathInput, + should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditMoviePathInput, + cursor_after_string: true, + }, ); draw_text_box_with_label( f, - chunks[5], - "Tags", - &tags.text, - *tags.offset.borrow(), - selected_block == &ActiveRadarrBlock::EditMovieTagsInput, - active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput, + LabeledTextBoxProps { + area: chunks[5], + label: "Tags", + text: &tags.text, + offset: *tags.offset.borrow(), + is_selected: selected_block == &ActiveRadarrBlock::EditMovieTagsInput, + should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput, + cursor_after_string: true, + }, ); } diff --git a/src/ui/utils.rs b/src/ui/utils.rs index f3a5d56..7202c12 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -267,8 +267,18 @@ pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge<'_> { .label(Line::from(format!("{title}: {:.0}%", ratio * 100.0))) } -pub fn show_cursor(f: &mut Frame<'_>, area: Rect, offset: usize, string: &str) { - f.set_cursor(area.x + (string.len() - offset) as u16 + 1, area.y + 1); +pub fn show_cursor( + f: &mut Frame<'_>, + area: Rect, + offset: usize, + string: &str, + cursor_after_string: bool, +) { + if cursor_after_string { + f.set_cursor(area.x + (string.len() - offset) as u16 + 1, area.y + 1); + } else { + f.set_cursor(area.x + 1u16, area.y + 1); + } } pub fn get_width_from_percentage(area: Rect, percentage: u16) -> usize {