diff --git a/Cargo.lock b/Cargo.lock index 6e6b6f9..407b30a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -863,7 +863,7 @@ dependencies = [ [[package]] name = "managarr" -version = "0.0.33" +version = "0.0.34" dependencies = [ "anyhow", "backtrace", diff --git a/Cargo.toml b/Cargo.toml index c6d588d..823ce31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managarr" -version = "0.0.33" +version = "0.0.34" authors = ["Alex Clarke "] description = "A TUI to manage your Servarrs" keywords = ["managarr", "tui-rs", "dashboard", "servarr", "tui"] diff --git a/README.md b/README.md index 547d490..971e1a9 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ pleasant as possible! ### Radarr -- [x] View your library, downloads, collections +- [x] View your library, downloads, collections, and blocklist - [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 @@ -48,6 +48,7 @@ pleasant as possible! - [x] Edit your movies, collections, and indexers - [x] Manage your tags - [x] Manage your root folders +- [x] Manage your blocklist - [ ] Manage your quality profiles - [ ] Manage your quality definitions - [x] View and browse logs, tasks, events queues, and updates @@ -144,6 +145,8 @@ tautulli: ![logs](screenshots/logs.png) ![new_movie_search](screenshots/new_movie_search.png) ![add_new_movie](screenshots/add_new_movie.png) +![collection_details](screenshots/collection_details.png) +![indexers](screenshots/indexers.png) ## Dependencies * [ratatui](https://github.com/tui-rs-revival/ratatui) diff --git a/screenshots/add_new_movie.png b/screenshots/add_new_movie.png index 5a87e36..6f99e5d 100644 Binary files a/screenshots/add_new_movie.png and b/screenshots/add_new_movie.png differ diff --git a/screenshots/collection_details.png b/screenshots/collection_details.png new file mode 100644 index 0000000..fde32a5 Binary files /dev/null and b/screenshots/collection_details.png differ diff --git a/screenshots/indexers.png b/screenshots/indexers.png new file mode 100644 index 0000000..3a30fff Binary files /dev/null and b/screenshots/indexers.png differ diff --git a/screenshots/library.png b/screenshots/library.png index 03da014..e589427 100644 Binary files a/screenshots/library.png and b/screenshots/library.png differ diff --git a/screenshots/logs.png b/screenshots/logs.png index f8d15d8..7beedb5 100644 Binary files a/screenshots/logs.png and b/screenshots/logs.png differ diff --git a/screenshots/manual_search.png b/screenshots/manual_search.png index d4bd2ab..99443af 100644 Binary files a/screenshots/manual_search.png and b/screenshots/manual_search.png differ diff --git a/screenshots/new_movie_search.png b/screenshots/new_movie_search.png index 7b44f62..afd358c 100644 Binary files a/screenshots/new_movie_search.png and b/screenshots/new_movie_search.png differ diff --git a/src/app/key_binding.rs b/src/app/key_binding.rs index a568975..712628a 100644 --- a/src/app/key_binding.rs +++ b/src/app/key_binding.rs @@ -15,6 +15,7 @@ generate_keybindings! { left, right, backspace, + clear, search, settings, filter, @@ -67,6 +68,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { key: Key::Backspace, desc: "backspace", }, + clear: KeyBinding { + key: Key::Char('c'), + desc: "clear", + }, search: KeyBinding { key: Key::Char('s'), desc: "search", diff --git a/src/app/key_binding_tests.rs b/src/app/key_binding_tests.rs index 05027aa..c9a776a 100644 --- a/src/app/key_binding_tests.rs +++ b/src/app/key_binding_tests.rs @@ -13,6 +13,7 @@ mod test { #[case(DEFAULT_KEYBINDINGS.left, Key::Left, "left")] #[case(DEFAULT_KEYBINDINGS.right, Key::Right, "right")] #[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, "backspace")] + #[case(DEFAULT_KEYBINDINGS.clear, Key::Char('c'), "clear")] #[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), "search")] #[case(DEFAULT_KEYBINDINGS.settings, Key::Char('s'), "settings")] #[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), "filter")] diff --git a/src/app/radarr/mod.rs b/src/app/radarr/mod.rs index c7513aa..1c97a13 100644 --- a/src/app/radarr/mod.rs +++ b/src/app/radarr/mod.rs @@ -11,6 +11,11 @@ mod radarr_tests; impl<'a> App<'a> { pub(super) async fn dispatch_by_radarr_block(&mut self, active_radarr_block: &ActiveRadarrBlock) { match active_radarr_block { + ActiveRadarrBlock::Blocklist => { + self + .dispatch_network_event(RadarrEvent::GetBlocklist.into()) + .await; + } ActiveRadarrBlock::Collections => { self .dispatch_network_event(RadarrEvent::GetCollections.into()) diff --git a/src/app/radarr/radarr_context_clues.rs b/src/app/radarr/radarr_context_clues.rs index 9a3335b..daa17cc 100644 --- a/src/app/radarr/radarr_context_clues.rs +++ b/src/app/radarr/radarr_context_clues.rs @@ -21,14 +21,6 @@ pub static LIBRARY_CONTEXT_CLUES: [ContextClue; 10] = [ (DEFAULT_KEYBINDINGS.esc, "cancel filter"), ]; -pub static DOWNLOADS_CONTEXT_CLUES: [ContextClue; 2] = [ - ( - DEFAULT_KEYBINDINGS.refresh, - DEFAULT_KEYBINDINGS.refresh.desc, - ), - (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), -]; - pub static COLLECTIONS_CONTEXT_CLUES: [ContextClue; 8] = [ (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), @@ -43,6 +35,25 @@ pub static COLLECTIONS_CONTEXT_CLUES: [ContextClue; 8] = [ (DEFAULT_KEYBINDINGS.esc, "cancel filter"), ]; +pub static DOWNLOADS_CONTEXT_CLUES: [ContextClue; 2] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), +]; + +pub static BLOCKLIST_CONTEXT_CLUES: [ContextClue; 5] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), + (DEFAULT_KEYBINDINGS.clear, "clear blocklist"), +]; + pub static ROOT_FOLDERS_CONTEXT_CLUES: [ContextClue; 3] = [ (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), (DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc), @@ -52,8 +63,7 @@ pub static ROOT_FOLDERS_CONTEXT_CLUES: [ContextClue; 3] = [ ), ]; -pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 7] = [ - (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), +pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [ (DEFAULT_KEYBINDINGS.submit, "edit indexer"), ( DEFAULT_KEYBINDINGS.settings, diff --git a/src/app/radarr/radarr_context_clues_tests.rs b/src/app/radarr/radarr_context_clues_tests.rs index 2f1f50c..7fc93bc 100644 --- a/src/app/radarr/radarr_context_clues_tests.rs +++ b/src/app/radarr/radarr_context_clues_tests.rs @@ -4,7 +4,7 @@ mod tests { use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::radarr_context_clues::{ - ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, + ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, COLLECTION_DETAILS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, @@ -67,22 +67,6 @@ mod tests { assert_eq!(library_context_clues_iter.next(), None); } - #[test] - fn test_downloads_context_clues() { - let mut downloads_context_clues_iter = DOWNLOADS_CONTEXT_CLUES.iter(); - - let (key_binding, description) = downloads_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); - - let (key_binding, description) = downloads_context_clues_iter.next().unwrap(); - - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); - assert_eq!(downloads_context_clues_iter.next(), None); - } - #[test] fn test_collections_context_clues() { let mut collections_context_clues = COLLECTIONS_CONTEXT_CLUES.iter(); @@ -129,6 +113,53 @@ mod tests { assert_eq!(collections_context_clues.next(), None); } + #[test] + fn test_downloads_context_clues() { + let mut downloads_context_clues_iter = DOWNLOADS_CONTEXT_CLUES.iter(); + + let (key_binding, description) = downloads_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = downloads_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); + assert_eq!(downloads_context_clues_iter.next(), None); + } + + #[test] + fn test_blocklist_context_clues() { + let mut blocklist_context_clues_iter = BLOCKLIST_CONTEXT_CLUES.iter(); + + let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); + + let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "details"); + + let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc); + + let (key_binding, description) = blocklist_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.clear); + assert_str_eq!(*description, "clear blocklist"); + assert_eq!(blocklist_context_clues_iter.next(), None); + } + #[test] fn test_root_folders_context_clues() { let mut root_folders_context_clues_iter = ROOT_FOLDERS_CONTEXT_CLUES.iter(); @@ -156,11 +187,6 @@ mod tests { let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.add); - assert_str_eq!(*description, DEFAULT_KEYBINDINGS.add.desc); - - let (key_binding, description) = indexers_context_clues_iter.next().unwrap(); - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); assert_str_eq!(*description, "edit indexer"); diff --git a/src/app/radarr/radarr_tests.rs b/src/app/radarr/radarr_tests.rs index 71f9275..c43613e 100644 --- a/src/app/radarr/radarr_tests.rs +++ b/src/app/radarr/radarr_tests.rs @@ -12,6 +12,23 @@ mod tests { use crate::network::radarr_network::RadarrEvent; use crate::network::NetworkEvent; + #[tokio::test] + async fn test_dispatch_by_blocklist_block() { + let (mut app, mut sync_network_rx) = construct_app_unit(); + + app + .dispatch_by_radarr_block(&ActiveRadarrBlock::Blocklist) + .await; + + assert!(app.is_loading); + assert_eq!( + sync_network_rx.recv().await.unwrap(), + RadarrEvent::GetBlocklist.into() + ); + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + #[tokio::test] async fn test_dispatch_by_collections_block() { let (mut app, mut sync_network_rx) = construct_app_unit(); diff --git a/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs new file mode 100644 index 0000000..278e5a7 --- /dev/null +++ b/src/handlers/radarr_handlers/blocklist/blocklist_handler_tests.rs @@ -0,0 +1,707 @@ +#[cfg(test)] +mod tests { + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::radarr_handlers::blocklist::{blocklist_sorting_options, BlocklistHandler}; + use crate::handlers::KeyEventHandler; + use crate::models::radarr_models::{BlocklistItem, Language, Movie, Quality, QualityWrapper}; + use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; + use crate::models::stateful_table::SortOption; + use chrono::DateTime; + use pretty_assertions::{assert_eq, assert_str_eq}; + use std::cmp::Ordering; + use strum::IntoEnumIterator; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + + use crate::models::radarr_models::BlocklistItem; + use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; + + use super::*; + + test_iterable_scroll!( + test_blocklist_scroll, + BlocklistHandler, + blocklist, + simple_stateful_iterable_vec!(BlocklistItem, String, source_title), + ActiveRadarrBlock::Blocklist, + None, + source_title, + to_string + ); + + #[rstest] + fn test_blocklist_sort_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let blocklist_field_vec = sort_options(); + let mut app = App::default(); + app.data.radarr_data.blocklist.sorting(sort_options()); + + if key == Key::Up { + for i in (0..blocklist_field_vec.len()).rev() { + BlocklistHandler::with( + &key, + &mut app, + &ActiveRadarrBlock::BlocklistSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .blocklist + .sort + .as_ref() + .unwrap() + .current_selection(), + &blocklist_field_vec[i] + ); + } + } else { + for i in 0..blocklist_field_vec.len() { + BlocklistHandler::with( + &key, + &mut app, + &ActiveRadarrBlock::BlocklistSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .blocklist + .sort + .as_ref() + .unwrap() + .current_selection(), + &blocklist_field_vec[(i + 1) % blocklist_field_vec.len()] + ); + } + } + } + } + + mod test_handle_home_end { + use super::*; + use crate::models::radarr_models::BlocklistItem; + use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; + use pretty_assertions::{assert_eq, assert_str_eq}; + + test_iterable_home_and_end!( + test_blocklist_home_and_end, + BlocklistHandler, + blocklist, + extended_stateful_iterable_vec!(BlocklistItem, String, source_title), + ActiveRadarrBlock::Blocklist, + None, + source_title, + to_string + ); + + #[test] + fn test_blocklist_sort_home_end() { + let blocklist_field_vec = sort_options(); + let mut app = App::default(); + app.data.radarr_data.blocklist.sorting(sort_options()); + + BlocklistHandler::with( + &DEFAULT_KEYBINDINGS.end.key, + &mut app, + &ActiveRadarrBlock::BlocklistSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .blocklist + .sort + .as_ref() + .unwrap() + .current_selection(), + &blocklist_field_vec[blocklist_field_vec.len() - 1] + ); + + BlocklistHandler::with( + &DEFAULT_KEYBINDINGS.home.key, + &mut app, + &ActiveRadarrBlock::BlocklistSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app + .data + .radarr_data + .blocklist + .sort + .as_ref() + .unwrap() + .current_selection(), + &blocklist_field_vec[0] + ); + } + } + + mod test_handle_delete { + use super::*; + use crate::assert_delete_prompt; + use pretty_assertions::assert_eq; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_blocklist_item_prompt() { + assert_delete_prompt!( + BlocklistHandler, + ActiveRadarrBlock::Blocklist, + ActiveRadarrBlock::DeleteBlocklistItemPrompt + ); + } + } + + mod test_handle_left_right_action { + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[test] + fn test_blocklist_tab_left() { + let mut app = App::default(); + app.data.radarr_data.main_tabs.set_index(3); + + BlocklistHandler::with( + &DEFAULT_KEYBINDINGS.left.key, + &mut app, + &ActiveRadarrBlock::Blocklist, + &None, + ) + .handle(); + + assert_eq!( + app.data.radarr_data.main_tabs.get_active_route(), + &ActiveRadarrBlock::Downloads.into() + ); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Downloads.into() + ); + } + + #[test] + fn test_blocklist_tab_right() { + let mut app = App::default(); + app.data.radarr_data.main_tabs.set_index(3); + + BlocklistHandler::with( + &DEFAULT_KEYBINDINGS.right.key, + &mut app, + &ActiveRadarrBlock::Blocklist, + &None, + ) + .handle(); + + assert_eq!( + app.data.radarr_data.main_tabs.get_active_route(), + &ActiveRadarrBlock::RootFolders.into() + ); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::RootFolders.into() + ); + } + + #[rstest] + fn test_blocklist_left_right_prompt_toggle( + #[values( + ActiveRadarrBlock::DeleteBlocklistItemPrompt, + ActiveRadarrBlock::BlocklistClearAllItemsPrompt + )] + active_radarr_block: ActiveRadarrBlock, + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::default(); + + BlocklistHandler::with(&key, &mut app, &active_radarr_block, &None).handle(); + + assert!(app.data.radarr_data.prompt_confirm); + + BlocklistHandler::with(&key, &mut app, &active_radarr_block, &None).handle(); + + assert!(!app.data.radarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use crate::network::radarr_network::RadarrEvent; + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[rstest] + #[case( + ActiveRadarrBlock::Blocklist, + ActiveRadarrBlock::DeleteBlocklistItemPrompt, + RadarrEvent::DeleteBlocklistItem + )] + #[case( + ActiveRadarrBlock::Blocklist, + ActiveRadarrBlock::BlocklistClearAllItemsPrompt, + RadarrEvent::ClearBlocklist + )] + fn test_blocklist_prompt_confirm_submit( + #[case] base_route: ActiveRadarrBlock, + #[case] prompt_block: ActiveRadarrBlock, + #[case] expected_action: RadarrEvent, + ) { + let mut app = App::default(); + app.data.radarr_data.prompt_confirm = true; + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + BlocklistHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle(); + + assert!(app.data.radarr_data.prompt_confirm); + assert_eq!( + app.data.radarr_data.prompt_confirm_action, + Some(expected_action) + ); + assert_eq!(app.get_current_route(), &base_route.into()); + } + + #[rstest] + fn test_blocklist_prompt_decline_submit( + #[values( + ActiveRadarrBlock::DeleteBlocklistItemPrompt, + ActiveRadarrBlock::BlocklistClearAllItemsPrompt + )] + prompt_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); + app.push_navigation_stack(prompt_block.into()); + + BlocklistHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle(); + + assert!(!app.data.radarr_data.prompt_confirm); + assert_eq!(app.data.radarr_data.prompt_confirm_action, None); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Blocklist.into() + ); + } + + #[test] + fn test_blocklist_sort_prompt_submit() { + let mut app = App::default(); + app.data.radarr_data.blocklist.sort_asc = true; + app.data.radarr_data.blocklist.sorting(sort_options()); + app.data.radarr_data.blocklist.set_items(blocklist_vec()); + app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); + app.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into()); + + let mut expected_vec = blocklist_vec(); + expected_vec.sort_by(|a, b| a.id.cmp(&b.id)); + expected_vec.reverse(); + + BlocklistHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::BlocklistSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Blocklist.into() + ); + assert_eq!(app.data.radarr_data.blocklist.items, expected_vec); + } + } + + mod test_handle_esc { + use super::*; + use crate::handlers::radarr_handlers::downloads::DownloadsHandler; + use pretty_assertions::assert_eq; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + #[case( + ActiveRadarrBlock::Blocklist, + ActiveRadarrBlock::DeleteBlocklistItemPrompt + )] + #[case( + ActiveRadarrBlock::Blocklist, + ActiveRadarrBlock::BlocklistClearAllItemsPrompt + )] + fn test_blocklist_prompt_blocks_esc( + #[case] base_block: ActiveRadarrBlock, + #[case] prompt_block: ActiveRadarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(base_block.into()); + app.push_navigation_stack(prompt_block.into()); + app.data.radarr_data.prompt_confirm = true; + + BlocklistHandler::with(&ESC_KEY, &mut app, &prompt_block, &None).handle(); + + assert_eq!(app.get_current_route(), &base_block.into()); + assert!(!app.data.radarr_data.prompt_confirm); + } + + #[test] + fn test_esc_blocklist_item_details() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); + app.push_navigation_stack(ActiveRadarrBlock::BlocklistItemDetails.into()); + + BlocklistHandler::with( + &ESC_KEY, + &mut app, + &ActiveRadarrBlock::BlocklistItemDetails, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Blocklist.into() + ); + } + + #[test] + fn test_blocklist_sort_prompt_block_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); + app.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into()); + + BlocklistHandler::with( + &ESC_KEY, + &mut app, + &ActiveRadarrBlock::BlocklistSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Blocklist.into() + ); + } + + #[test] + fn test_default_esc() { + let mut app = App::default(); + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); + app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into()); + + DownloadsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Blocklist.into() + ); + assert!(app.error.text.is_empty()); + } + } + + mod test_handle_key_char { + use super::*; + use crate::assert_refresh_key; + use pretty_assertions::assert_eq; + + #[test] + fn test_refresh_blocklist_key() { + assert_refresh_key!(BlocklistHandler, ActiveRadarrBlock::Blocklist); + } + + #[test] + fn test_clear_blocklist_key() { + let mut app = App::default(); + + BlocklistHandler::with( + &DEFAULT_KEYBINDINGS.clear.key, + &mut app, + &ActiveRadarrBlock::Blocklist, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into() + ); + } + + #[test] + fn test_sort_key() { + let mut app = App::default(); + + BlocklistHandler::with( + &DEFAULT_KEYBINDINGS.sort.key, + &mut app, + &ActiveRadarrBlock::Blocklist, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::BlocklistSortPrompt.into() + ); + assert_eq!( + app.data.radarr_data.blocklist.sort.as_ref().unwrap().items, + blocklist_sorting_options() + ); + assert!(!app.data.radarr_data.blocklist.sort_asc); + } + } + + #[test] + fn test_blocklist_sorting_options_movie_title() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + a.movie + .title + .text + .to_lowercase() + .cmp(&b.movie.title.text.to_lowercase()) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[0].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Movie Title"); + } + + #[test] + fn test_blocklist_sorting_options_source_title() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + a.source_title + .to_lowercase() + .cmp(&b.source_title.to_lowercase()) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[1].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Source Title"); + } + + #[test] + fn test_blocklist_sorting_options_languages() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + let a_languages = a + .languages + .iter() + .map(|lang| lang.name.to_lowercase()) + .collect::>() + .join(", "); + let b_languages = b + .languages + .iter() + .map(|lang| lang.name.to_lowercase()) + .collect::>() + .join(", "); + + a_languages.cmp(&b_languages) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[2].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Languages"); + } + + #[test] + fn test_blocklist_sorting_options_quality() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[3].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + #[test] + fn test_blocklist_sorting_options_custom_formats() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| { + let a_custom_formats = a + .custom_formats + .as_ref() + .unwrap_or(&Vec::new()) + .iter() + .map(|lang| lang.name.to_lowercase()) + .collect::>() + .join(", "); + let b_custom_formats = b + .custom_formats + .as_ref() + .unwrap_or(&Vec::new()) + .iter() + .map(|lang| lang.name.to_lowercase()) + .collect::>() + .join(", "); + + a_custom_formats.cmp(&b_custom_formats) + }; + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[4].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Formats"); + } + + #[test] + fn test_blocklist_sorting_options_date() { + let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = + |a, b| a.date.cmp(&b.date); + let mut expected_blocklist_vec = blocklist_vec(); + expected_blocklist_vec.sort_by(expected_cmp_fn); + + let sort_option = blocklist_sorting_options()[5].clone(); + let mut sorted_blocklist_vec = blocklist_vec(); + sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_blocklist_vec, expected_blocklist_vec); + assert_str_eq!(sort_option.name, "Date"); + } + + fn blocklist_vec() -> Vec { + vec![ + BlocklistItem { + id: 3, + source_title: "test 1".to_owned(), + languages: vec![Language { + name: "telgu".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "HD - 1080p".to_owned(), + }, + }, + custom_formats: Some(vec![Language { + name: "nikki".to_owned(), + }]), + date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()), + movie: Movie { + title: "test 3".into(), + ..Movie::default() + }, + ..BlocklistItem::default() + }, + BlocklistItem { + id: 2, + source_title: "test 2".to_owned(), + languages: vec![Language { + name: "chinese".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "SD - 720p".to_owned(), + }, + }, + custom_formats: Some(vec![ + Language { + name: "alex".to_owned(), + }, + Language { + name: "English".to_owned(), + }, + ]), + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + movie: Movie { + title: "test 2".into(), + ..Movie::default() + }, + ..BlocklistItem::default() + }, + BlocklistItem { + id: 1, + source_title: "test 3".to_owned(), + languages: vec![Language { + name: "english".to_owned(), + }], + quality: QualityWrapper { + quality: Quality { + name: "HD - 1080p".to_owned(), + }, + }, + custom_formats: Some(vec![Language { + name: "English".to_owned(), + }]), + date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()), + movie: Movie { + title: "test 1".into(), + ..Movie::default() + }, + ..BlocklistItem::default() + }, + ] + } + + fn sort_options() -> Vec> { + vec![SortOption { + name: "Test 1", + cmp_fn: Some(|a, b| { + b.source_title + .to_lowercase() + .cmp(&a.source_title.to_lowercase()) + }), + }] + } + + #[test] + fn test_blocklist_handler_accepts() { + ActiveRadarrBlock::iter().for_each(|active_radarr_block| { + if BLOCKLIST_BLOCKS.contains(&active_radarr_block) { + assert!(BlocklistHandler::accepts(&active_radarr_block)); + } else { + assert!(!BlocklistHandler::accepts(&active_radarr_block)); + } + }) + } +} diff --git a/src/handlers/radarr_handlers/blocklist/mod.rs b/src/handlers/radarr_handlers/blocklist/mod.rs new file mode 100644 index 0000000..623a084 --- /dev/null +++ b/src/handlers/radarr_handlers/blocklist/mod.rs @@ -0,0 +1,284 @@ +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::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; +use crate::models::radarr_models::BlocklistItem; +use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; +use crate::models::stateful_table::SortOption; +use crate::models::Scrollable; +use crate::network::radarr_network::RadarrEvent; + +#[cfg(test)] +#[path = "blocklist_handler_tests.rs"] +mod blocklist_handler_tests; + +pub(super) struct BlocklistHandler<'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 BlocklistHandler<'a, 'b> { + fn accepts(active_block: &'a ActiveRadarrBlock) -> bool { + BLOCKLIST_BLOCKS.contains(active_block) + } + + fn with( + key: &'a Key, + app: &'a mut App<'b>, + active_block: &'a ActiveRadarrBlock, + context: &'a Option, + ) -> Self { + BlocklistHandler { + key, + app, + active_radarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> &Key { + self.key + } + + fn handle_scroll_up(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_up(), + ActiveRadarrBlock::BlocklistSortPrompt => self + .app + .data + .radarr_data + .blocklist + .sort + .as_mut() + .unwrap() + .scroll_up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_down(), + ActiveRadarrBlock::BlocklistSortPrompt => self + .app + .data + .radarr_data + .blocklist + .sort + .as_mut() + .unwrap() + .scroll_down(), + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_to_top(), + ActiveRadarrBlock::BlocklistSortPrompt => self + .app + .data + .radarr_data + .blocklist + .sort + .as_mut() + .unwrap() + .scroll_to_top(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_to_bottom(), + ActiveRadarrBlock::BlocklistSortPrompt => self + .app + .data + .radarr_data + .blocklist + .sort + .as_mut() + .unwrap() + .scroll_to_bottom(), + _ => (), + } + } + + fn handle_delete(&mut self) { + if self.active_radarr_block == &ActiveRadarrBlock::Blocklist { + self + .app + .push_navigation_stack(ActiveRadarrBlock::DeleteBlocklistItemPrompt.into()); + } + } + + fn handle_left_right_action(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::Blocklist => handle_change_tab_left_right_keys(self.app, self.key), + ActiveRadarrBlock::DeleteBlocklistItemPrompt + | ActiveRadarrBlock::BlocklistClearAllItemsPrompt => handle_prompt_toggle(self.app, self.key), + _ => {} + } + } + + fn handle_submit(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::DeleteBlocklistItemPrompt => { + if self.app.data.radarr_data.prompt_confirm { + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteBlocklistItem); + } + + self.app.pop_navigation_stack(); + } + ActiveRadarrBlock::BlocklistClearAllItemsPrompt => { + if self.app.data.radarr_data.prompt_confirm { + self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist); + } + + self.app.pop_navigation_stack(); + } + ActiveRadarrBlock::BlocklistSortPrompt => { + self + .app + .data + .radarr_data + .blocklist + .items + .sort_by(|a, b| a.id.cmp(&b.id)); + self.app.data.radarr_data.blocklist.apply_sorting(); + + self.app.pop_navigation_stack(); + } + ActiveRadarrBlock::Blocklist => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::BlocklistItemDetails.into()); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_radarr_block { + ActiveRadarrBlock::DeleteBlocklistItemPrompt + | ActiveRadarrBlock::BlocklistClearAllItemsPrompt => { + self.app.pop_navigation_stack(); + self.app.data.radarr_data.prompt_confirm = false; + } + ActiveRadarrBlock::BlocklistItemDetails | ActiveRadarrBlock::BlocklistSortPrompt => { + self.app.pop_navigation_stack(); + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + if self.active_radarr_block == &ActiveRadarrBlock::Blocklist { + match self.key { + _ if *key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ if *key == DEFAULT_KEYBINDINGS.clear.key => { + self + .app + .push_navigation_stack(ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into()); + } + _ if *key == DEFAULT_KEYBINDINGS.sort.key => { + self + .app + .data + .radarr_data + .blocklist + .sorting(blocklist_sorting_options()); + self + .app + .push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into()); + } + _ => (), + } + } + } +} + +fn blocklist_sorting_options() -> Vec> { + vec![ + SortOption { + name: "Movie Title", + cmp_fn: Some(|a, b| { + a.movie + .title + .text + .to_lowercase() + .cmp(&b.movie.title.text.to_lowercase()) + }), + }, + SortOption { + name: "Source Title", + cmp_fn: Some(|a, b| { + a.source_title + .to_lowercase() + .cmp(&b.source_title.to_lowercase()) + }), + }, + SortOption { + name: "Languages", + cmp_fn: Some(|a, b| { + let a_languages = a + .languages + .iter() + .map(|lang| lang.name.to_lowercase()) + .collect::>() + .join(", "); + let b_languages = b + .languages + .iter() + .map(|lang| lang.name.to_lowercase()) + .collect::>() + .join(", "); + + a_languages.cmp(&b_languages) + }), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }), + }, + SortOption { + name: "Formats", + cmp_fn: Some(|a, b| { + let a_custom_formats = a + .custom_formats + .as_ref() + .unwrap_or(&Vec::new()) + .iter() + .map(|lang| lang.name.to_lowercase()) + .collect::>() + .join(", "); + let b_custom_formats = b + .custom_formats + .as_ref() + .unwrap_or(&Vec::new()) + .iter() + .map(|lang| lang.name.to_lowercase()) + .collect::>() + .join(", "); + + a_custom_formats.cmp(&b_custom_formats) + }), + }, + SortOption { + name: "Date", + cmp_fn: Some(|a, b| a.date.cmp(&b.date)), + }, + ] +} diff --git a/src/handlers/radarr_handlers/collections/collections_handler_tests.rs b/src/handlers/radarr_handlers/collections/collections_handler_tests.rs index 03af63c..1fdd360 100644 --- a/src/handlers/radarr_handlers/collections/collections_handler_tests.rs +++ b/src/handlers/radarr_handlers/collections/collections_handler_tests.rs @@ -267,7 +267,7 @@ mod tests { #[test] fn test_collections_tab_left() { let mut app = App::default(); - app.data.radarr_data.main_tabs.set_index(2); + app.data.radarr_data.main_tabs.set_index(1); CollectionsHandler::with( &DEFAULT_KEYBINDINGS.left.key, @@ -279,18 +279,15 @@ mod tests { assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Downloads.into() - ); - assert_eq!( - app.get_current_route(), - &ActiveRadarrBlock::Downloads.into() + &ActiveRadarrBlock::Movies.into() ); + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); } #[test] fn test_collections_tab_right() { let mut app = App::default(); - app.data.radarr_data.main_tabs.set_index(2); + app.data.radarr_data.main_tabs.set_index(1); CollectionsHandler::with( &DEFAULT_KEYBINDINGS.right.key, @@ -302,11 +299,11 @@ mod tests { assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::RootFolders.into() + &ActiveRadarrBlock::Downloads.into() ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::RootFolders.into() + &ActiveRadarrBlock::Downloads.into() ); } @@ -832,6 +829,26 @@ mod tests { assert!(!app.data.radarr_data.prompt_confirm); } + #[test] + fn test_collections_sort_prompt_block_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Collections.into()); + app.push_navigation_stack(ActiveRadarrBlock::CollectionsSortPrompt.into()); + + CollectionsHandler::with( + &ESC_KEY, + &mut app, + &ActiveRadarrBlock::CollectionsSortPrompt, + &None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Collections.into() + ); + } + #[test] fn test_default_esc() { let mut app = App::default(); diff --git a/src/handlers/radarr_handlers/collections/mod.rs b/src/handlers/radarr_handlers/collections/mod.rs index a273700..77d6226 100644 --- a/src/handlers/radarr_handlers/collections/mod.rs +++ b/src/handlers/radarr_handlers/collections/mod.rs @@ -287,6 +287,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<' self.app.pop_navigation_stack(); self.app.data.radarr_data.prompt_confirm = false; } + ActiveRadarrBlock::CollectionsSortPrompt => { + self.app.pop_navigation_stack(); + } _ => { self.app.data.radarr_data.collections.reset_search(); self.app.data.radarr_data.collections.reset_filter(); diff --git a/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs b/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs index 40de244..c02475a 100644 --- a/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs +++ b/src/handlers/radarr_handlers/downloads/downloads_handler_tests.rs @@ -74,7 +74,7 @@ mod tests { #[test] fn test_downloads_tab_left() { let mut app = App::default(); - app.data.radarr_data.main_tabs.set_index(1); + app.data.radarr_data.main_tabs.set_index(2); DownloadsHandler::with( &DEFAULT_KEYBINDINGS.left.key, @@ -84,26 +84,6 @@ mod tests { ) .handle(); - assert_eq!( - app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Movies.into() - ); - assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); - } - - #[test] - fn test_downloads_tab_right() { - let mut app = App::default(); - app.data.radarr_data.main_tabs.set_index(1); - - DownloadsHandler::with( - &DEFAULT_KEYBINDINGS.right.key, - &mut app, - &ActiveRadarrBlock::Downloads, - &None, - ) - .handle(); - assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), &ActiveRadarrBlock::Collections.into() @@ -114,6 +94,29 @@ mod tests { ); } + #[test] + fn test_downloads_tab_right() { + let mut app = App::default(); + app.data.radarr_data.main_tabs.set_index(2); + + DownloadsHandler::with( + &DEFAULT_KEYBINDINGS.right.key, + &mut app, + &ActiveRadarrBlock::Downloads, + &None, + ) + .handle(); + + assert_eq!( + app.data.radarr_data.main_tabs.get_active_route(), + &ActiveRadarrBlock::Blocklist.into() + ); + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::Blocklist.into() + ); + } + #[rstest] fn test_downloads_left_right_prompt_toggle( #[values( diff --git a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs index aa2bd9e..c6076ce 100644 --- a/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs +++ b/src/handlers/radarr_handlers/indexers/indexers_handler_tests.rs @@ -78,7 +78,7 @@ mod tests { #[test] fn test_indexers_tab_left() { let mut app = App::default(); - app.data.radarr_data.main_tabs.set_index(4); + app.data.radarr_data.main_tabs.set_index(5); IndexersHandler::with( &DEFAULT_KEYBINDINGS.left.key, @@ -101,7 +101,7 @@ mod tests { #[test] fn test_indexers_tab_right() { let mut app = App::default(); - app.data.radarr_data.main_tabs.set_index(4); + app.data.radarr_data.main_tabs.set_index(5); IndexersHandler::with( &DEFAULT_KEYBINDINGS.right.key, diff --git a/src/handlers/radarr_handlers/library/library_handler_tests.rs b/src/handlers/radarr_handlers/library/library_handler_tests.rs index 4d9dac7..1ebc3a2 100644 --- a/src/handlers/radarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/library_handler_tests.rs @@ -312,11 +312,11 @@ mod tests { assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Downloads.into() + &ActiveRadarrBlock::Collections.into() ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Downloads.into() + &ActiveRadarrBlock::Collections.into() ); } @@ -776,6 +776,23 @@ mod tests { assert!(!app.data.radarr_data.prompt_confirm); } + #[test] + fn test_movies_sort_prompt_block_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); + app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into()); + + LibraryHandler::with( + &ESC_KEY, + &mut app, + &ActiveRadarrBlock::MoviesSortPrompt, + &None, + ) + .handle(); + + assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into()); + } + #[test] fn test_default_esc() { let mut app = App::default(); diff --git a/src/handlers/radarr_handlers/library/mod.rs b/src/handlers/radarr_handlers/library/mod.rs index 420635d..9edf6e0 100644 --- a/src/handlers/radarr_handlers/library/mod.rs +++ b/src/handlers/radarr_handlers/library/mod.rs @@ -298,6 +298,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, ' self.app.pop_navigation_stack(); self.app.data.radarr_data.prompt_confirm = false; } + ActiveRadarrBlock::MoviesSortPrompt => { + self.app.pop_navigation_stack(); + } _ => { self.app.data.radarr_data.movies.reset_search(); self.app.data.radarr_data.movies.reset_filter(); diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 19d05ce..2c4042a 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -1,4 +1,5 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::handlers::radarr_handlers::blocklist::BlocklistHandler; use crate::handlers::radarr_handlers::collections::CollectionsHandler; use crate::handlers::radarr_handlers::downloads::DownloadsHandler; use crate::handlers::radarr_handlers::indexers::IndexersHandler; @@ -9,6 +10,7 @@ use crate::handlers::KeyEventHandler; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::{App, Key}; +mod blocklist; mod collections; mod downloads; mod indexers; @@ -54,6 +56,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b RootFoldersHandler::with(self.key, self.app, self.active_radarr_block, self.context) .handle() } + _ if BlocklistHandler::accepts(self.active_radarr_block) => { + BlocklistHandler::with(self.key, self.app, self.active_radarr_block, self.context).handle() + } _ => self.handle_key_event(), } } diff --git a/src/handlers/radarr_handlers/radarr_handler_tests.rs b/src/handlers/radarr_handlers/radarr_handler_tests.rs index 430c133..6cc5d84 100644 --- a/src/handlers/radarr_handlers/radarr_handler_tests.rs +++ b/src/handlers/radarr_handlers/radarr_handler_tests.rs @@ -12,12 +12,13 @@ mod tests { use crate::test_handler_delegation; #[rstest] - #[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Downloads)] - #[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Collections)] - #[case(2, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)] - #[case(3, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Indexers)] - #[case(4, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)] - #[case(5, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)] + #[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Collections)] + #[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Downloads)] + #[case(2, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Blocklist)] + #[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)] + #[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Indexers)] + #[case(5, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)] + #[case(6, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)] fn test_radarr_handler_change_tab_left_right_keys( #[case] index: usize, #[case] left_block: ActiveRadarrBlock, @@ -68,6 +69,7 @@ mod tests { fn test_delegates_library_blocks_to_library_handler( #[values( ActiveRadarrBlock::Movies, + ActiveRadarrBlock::MoviesSortPrompt, ActiveRadarrBlock::SearchMovie, ActiveRadarrBlock::SearchMovieError, ActiveRadarrBlock::FilterMovies, @@ -112,6 +114,7 @@ mod tests { #[values( ActiveRadarrBlock::Collections, ActiveRadarrBlock::SearchCollection, + ActiveRadarrBlock::CollectionsSortPrompt, ActiveRadarrBlock::SearchCollectionError, ActiveRadarrBlock::FilterCollections, ActiveRadarrBlock::FilterCollectionsError, @@ -189,6 +192,24 @@ mod tests { ); } + #[rstest] + fn test_delegates_blocklist_blocks_to_blocklist_handler( + #[values( + ActiveRadarrBlock::Blocklist, + ActiveRadarrBlock::BlocklistItemDetails, + ActiveRadarrBlock::DeleteBlocklistItemPrompt, + ActiveRadarrBlock::BlocklistClearAllItemsPrompt, + ActiveRadarrBlock::BlocklistSortPrompt + )] + active_radarr_block: ActiveRadarrBlock, + ) { + test_handler_delegation!( + RadarrHandler, + ActiveRadarrBlock::Blocklist, + active_radarr_block + ); + } + #[test] fn test_radarr_handler_accepts() { ActiveRadarrBlock::iter().for_each(|active_radarr_block| { diff --git a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs index 60631f6..1ee2120 100644 --- a/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs +++ b/src/handlers/radarr_handlers/root_folders/root_folders_handler_tests.rs @@ -123,7 +123,7 @@ mod tests { #[test] fn test_root_folders_tab_left() { let mut app = App::default(); - app.data.radarr_data.main_tabs.set_index(3); + app.data.radarr_data.main_tabs.set_index(4); RootFoldersHandler::with( &DEFAULT_KEYBINDINGS.left.key, @@ -135,18 +135,18 @@ mod tests { assert_eq!( app.data.radarr_data.main_tabs.get_active_route(), - &ActiveRadarrBlock::Collections.into() + &ActiveRadarrBlock::Blocklist.into() ); assert_eq!( app.get_current_route(), - &ActiveRadarrBlock::Collections.into() + &ActiveRadarrBlock::Blocklist.into() ); } #[test] fn test_root_folders_tab_right() { let mut app = App::default(); - app.data.radarr_data.main_tabs.set_index(3); + app.data.radarr_data.main_tabs.set_index(4); RootFoldersHandler::with( &DEFAULT_KEYBINDINGS.right.key, diff --git a/src/handlers/radarr_handlers/system/system_details_handler_tests.rs b/src/handlers/radarr_handlers/system/system_details_handler_tests.rs index f23809a..bdf3b5a 100644 --- a/src/handlers/radarr_handlers/system/system_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/system/system_details_handler_tests.rs @@ -422,11 +422,6 @@ mod tests { let mut app = App::default(); app.push_navigation_stack(ActiveRadarrBlock::System.into()); app.push_navigation_stack(ActiveRadarrBlock::SystemUpdates.into()); - app - .data - .radarr_data - .queued_events - .set_items(vec![QueueEvent::default()]); SystemDetailsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::SystemUpdates, &None) .handle(); diff --git a/src/handlers/radarr_handlers/system/system_handler_tests.rs b/src/handlers/radarr_handlers/system/system_handler_tests.rs index 566c4bf..b7b759e 100644 --- a/src/handlers/radarr_handlers/system/system_handler_tests.rs +++ b/src/handlers/radarr_handlers/system/system_handler_tests.rs @@ -22,7 +22,7 @@ mod tests { #[test] fn test_system_tab_left() { let mut app = App::default(); - app.data.radarr_data.main_tabs.set_index(5); + app.data.radarr_data.main_tabs.set_index(6); SystemHandler::with( &DEFAULT_KEYBINDINGS.left.key, @@ -42,7 +42,7 @@ mod tests { #[test] fn test_system_tab_right() { let mut app = App::default(); - app.data.radarr_data.main_tabs.set_index(5); + app.data.radarr_data.main_tabs.set_index(6); SystemHandler::with( &DEFAULT_KEYBINDINGS.right.key, diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index ec7133c..86f1605 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -54,6 +54,29 @@ pub struct AddRootFolderBody { pub path: String, } +#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct BlocklistResponse { + pub records: Vec, +} + +#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BlocklistItem { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, + #[serde(deserialize_with = "super::from_i64")] + pub movie_id: i64, + pub source_title: String, + pub languages: Vec, + pub quality: QualityWrapper, + pub custom_formats: Option>, + pub date: DateTime, + pub protocol: String, + pub indexer: String, + pub message: String, + pub movie: Movie, +} + #[derive(Deserialize, Derivative, Default, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Collection { diff --git a/src/models/servarr_data/radarr/radarr_data.rs b/src/models/servarr_data/radarr/radarr_data.rs index 9fd936e..83a2aa4 100644 --- a/src/models/servarr_data/radarr/radarr_data.rs +++ b/src/models/servarr_data/radarr/radarr_data.rs @@ -1,13 +1,13 @@ use crate::app::context_clues::build_context_clue_string; use crate::app::radarr::radarr_context_clues::{ - COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, - LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, + BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }; use crate::models::radarr_models::{ - AddMovieSearchResult, Collection, CollectionMovie, DiskSpace, DownloadRecord, Indexer, - IndexerSettings, Movie, QueueEvent, RootFolder, Task, + AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DiskSpace, DownloadRecord, + Indexer, IndexerSettings, Movie, QueueEvent, RootFolder, Task, }; use crate::models::servarr_data::radarr::modals::{ AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem, @@ -40,6 +40,7 @@ pub struct RadarrData<'a> { pub selected_block: BlockSelectionState<'a, ActiveRadarrBlock>, pub downloads: StatefulTable, pub indexers: StatefulTable, + pub blocklist: StatefulTable, pub quality_profile_map: BiMap, pub tags_map: BiMap, pub collections: StatefulTable, @@ -91,6 +92,7 @@ impl<'a> Default for RadarrData<'a> { selected_block: BlockSelectionState::default(), downloads: StatefulTable::default(), indexers: StatefulTable::default(), + blocklist: StatefulTable::default(), quality_profile_map: BiMap::default(), tags_map: BiMap::default(), collections: StatefulTable::default(), @@ -122,6 +124,12 @@ impl<'a> Default for RadarrData<'a> { help: String::new(), contextual_help: Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES)), }, + TabRoute { + title: "Collections", + route: ActiveRadarrBlock::Collections.into(), + help: String::new(), + contextual_help: Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)), + }, TabRoute { title: "Downloads", route: ActiveRadarrBlock::Downloads.into(), @@ -129,10 +137,10 @@ impl<'a> Default for RadarrData<'a> { contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)), }, TabRoute { - title: "Collections", - route: ActiveRadarrBlock::Collections.into(), + title: "Blocklist", + route: ActiveRadarrBlock::Blocklist.into(), help: String::new(), - contextual_help: Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)), + contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)), }, TabRoute { title: "Root Folders", @@ -213,11 +221,16 @@ pub enum ActiveRadarrBlock { AddMovieEmptySearchResults, AddRootFolderPrompt, AutomaticallySearchMoviePrompt, + Blocklist, + BlocklistClearAllItemsPrompt, + BlocklistItemDetails, + BlocklistSortPrompt, Collections, CollectionsSortPrompt, CollectionDetails, Cast, Crew, + DeleteBlocklistItemPrompt, DeleteDownloadPrompt, DeleteIndexerPrompt, DeleteMoviePrompt, @@ -323,6 +336,13 @@ pub static ROOT_FOLDERS_BLOCKS: [ActiveRadarrBlock; 3] = [ ActiveRadarrBlock::AddRootFolderPrompt, ActiveRadarrBlock::DeleteRootFolderPrompt, ]; +pub static BLOCKLIST_BLOCKS: [ActiveRadarrBlock; 5] = [ + ActiveRadarrBlock::Blocklist, + ActiveRadarrBlock::BlocklistItemDetails, + ActiveRadarrBlock::DeleteBlocklistItemPrompt, + ActiveRadarrBlock::BlocklistClearAllItemsPrompt, + ActiveRadarrBlock::BlocklistSortPrompt, +]; pub static ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 10] = [ ActiveRadarrBlock::AddMovieSearchInput, ActiveRadarrBlock::AddMovieSearchResults, diff --git a/src/models/servarr_data/radarr/radarr_data_tests.rs b/src/models/servarr_data/radarr/radarr_data_tests.rs index ec38c60..e983dcb 100644 --- a/src/models/servarr_data/radarr/radarr_data_tests.rs +++ b/src/models/servarr_data/radarr/radarr_data_tests.rs @@ -6,8 +6,8 @@ mod tests { use crate::app::context_clues::build_context_clue_string; use crate::app::radarr::radarr_context_clues::{ - COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, - LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, + BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, + INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES, }; @@ -64,6 +64,7 @@ mod tests { assert_eq!(radarr_data.selected_block, BlockSelectionState::default()); assert!(radarr_data.downloads.items.is_empty()); assert!(radarr_data.indexers.items.is_empty()); + assert!(radarr_data.blocklist.items.is_empty()); assert!(radarr_data.quality_profile_map.is_empty()); assert!(radarr_data.tags_map.is_empty()); assert!(radarr_data.collections.items.is_empty()); @@ -89,7 +90,7 @@ mod tests { assert!(!radarr_data.delete_movie_files); assert!(!radarr_data.add_list_exclusion); - assert_eq!(radarr_data.main_tabs.tabs.len(), 6); + assert_eq!(radarr_data.main_tabs.tabs.len(), 7); assert_str_eq!(radarr_data.main_tabs.tabs[0].title, "Library"); assert_eq!( @@ -102,58 +103,69 @@ mod tests { Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES)) ); - assert_str_eq!(radarr_data.main_tabs.tabs[1].title, "Downloads"); + assert_str_eq!(radarr_data.main_tabs.tabs[1].title, "Collections"); assert_eq!( radarr_data.main_tabs.tabs[1].route, - ActiveRadarrBlock::Downloads.into() + ActiveRadarrBlock::Collections.into() ); assert!(radarr_data.main_tabs.tabs[1].help.is_empty()); assert_eq!( radarr_data.main_tabs.tabs[1].contextual_help, - Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)) + Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)) ); - assert_str_eq!(radarr_data.main_tabs.tabs[2].title, "Collections"); + assert_str_eq!(radarr_data.main_tabs.tabs[2].title, "Downloads"); assert_eq!( radarr_data.main_tabs.tabs[2].route, - ActiveRadarrBlock::Collections.into() + ActiveRadarrBlock::Downloads.into() ); assert!(radarr_data.main_tabs.tabs[2].help.is_empty()); assert_eq!( radarr_data.main_tabs.tabs[2].contextual_help, - Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)) + Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)) ); - assert_str_eq!(radarr_data.main_tabs.tabs[3].title, "Root Folders"); + assert_str_eq!(radarr_data.main_tabs.tabs[3].title, "Blocklist"); assert_eq!( radarr_data.main_tabs.tabs[3].route, - ActiveRadarrBlock::RootFolders.into() + ActiveRadarrBlock::Blocklist.into() ); assert!(radarr_data.main_tabs.tabs[3].help.is_empty()); assert_eq!( radarr_data.main_tabs.tabs[3].contextual_help, - Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)) + Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)) ); - assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "Indexers"); + assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "Root Folders"); assert_eq!( radarr_data.main_tabs.tabs[4].route, - ActiveRadarrBlock::Indexers.into() + ActiveRadarrBlock::RootFolders.into() ); assert!(radarr_data.main_tabs.tabs[4].help.is_empty()); assert_eq!( radarr_data.main_tabs.tabs[4].contextual_help, - Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)) + Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES)) ); - assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "System"); + assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "Indexers"); assert_eq!( radarr_data.main_tabs.tabs[5].route, - ActiveRadarrBlock::System.into() + ActiveRadarrBlock::Indexers.into() ); assert!(radarr_data.main_tabs.tabs[5].help.is_empty()); assert_eq!( radarr_data.main_tabs.tabs[5].contextual_help, + Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES)) + ); + + assert_str_eq!(radarr_data.main_tabs.tabs[6].title, "System"); + assert_eq!( + radarr_data.main_tabs.tabs[6].route, + ActiveRadarrBlock::System.into() + ); + assert!(radarr_data.main_tabs.tabs[6].help.is_empty()); + assert_eq!( + radarr_data.main_tabs.tabs[6].contextual_help, Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES)) ); @@ -246,10 +258,10 @@ mod tests { use pretty_assertions::assert_eq; use crate::models::servarr_data::radarr::radarr_data::{ - 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_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS, + ActiveRadarrBlock, ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, BLOCKLIST_BLOCKS, + COLLECTIONS_BLOCKS, COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS, + DELETE_MOVIE_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_COLLECTION_BLOCKS, + EDIT_COLLECTION_SELECTION_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, @@ -296,6 +308,16 @@ mod tests { assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveRadarrBlock::DeleteRootFolderPrompt)); } + #[test] + fn test_blocklist_blocks_contents() { + assert_eq!(BLOCKLIST_BLOCKS.len(), 5); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::Blocklist)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::BlocklistItemDetails)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::DeleteBlocklistItemPrompt)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::BlocklistClearAllItemsPrompt)); + assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::BlocklistSortPrompt)); + } + #[test] fn test_add_movie_blocks_contents() { assert_eq!(ADD_MOVIE_BLOCKS.len(), 10); diff --git a/src/network/mod.rs b/src/network/mod.rs index 12af9b8..216f793 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -143,7 +143,11 @@ impl<'a, 'b> Network<'a, 'b> { .put(uri) .json(&body.unwrap_or_default()) .header("X-Api-Key", api_token), - RequestMethod::Delete => self.client.delete(uri).header("X-Api-Key", api_token), + RequestMethod::Delete => self + .client + .delete(uri) + .json(&body.unwrap_or_default()) + .header("X-Api-Key", api_token), } } } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 5d66398..274a105 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -9,11 +9,11 @@ use urlencoding::encode; use crate::app::RadarrConfig; use crate::models::radarr_models::{ - AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, Collection, CollectionMovie, - CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, Indexer, - IndexerSettings, IndexerTestResult, LogResponse, Movie, MovieCommandBody, MovieHistoryItem, - QualityProfile, QueueEvent, Release, ReleaseDownloadBody, RootFolder, SystemStatus, Tag, Task, - Update, + AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, BlocklistResponse, Collection, + CollectionMovie, CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, + Indexer, IndexerSettings, IndexerTestResult, LogResponse, Movie, MovieCommandBody, + MovieHistoryItem, QualityProfile, QueueEvent, Release, ReleaseDownloadBody, RootFolder, + SystemStatus, Tag, Task, Update, }; use crate::models::servarr_data::radarr::modals::{ AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem, @@ -33,6 +33,8 @@ mod radarr_network_tests; pub enum RadarrEvent { AddMovie, AddRootFolder, + ClearBlocklist, + DeleteBlocklistItem, DeleteDownload, DeleteIndexer, DeleteMovie, @@ -42,6 +44,7 @@ pub enum RadarrEvent { EditCollection, EditIndexer, EditMovie, + GetBlocklist, GetCollections, GetDownloads, GetIndexers, @@ -75,6 +78,9 @@ pub enum RadarrEvent { impl RadarrEvent { const fn resource(self) -> &'static str { match self { + RadarrEvent::ClearBlocklist => "/blocklist/bulk", + RadarrEvent::DeleteBlocklistItem => "/blocklist", + RadarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000", RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection", RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue", RadarrEvent::GetIndexers | RadarrEvent::EditIndexer | RadarrEvent::DeleteIndexer => { @@ -125,6 +131,8 @@ impl<'a, 'b> Network<'a, 'b> { match radarr_event { RadarrEvent::AddMovie => self.add_movie().await, RadarrEvent::AddRootFolder => self.add_root_folder().await, + RadarrEvent::ClearBlocklist => self.clear_blocklist().await, + RadarrEvent::DeleteBlocklistItem => self.delete_blocklist_item().await, RadarrEvent::DeleteDownload => self.delete_download().await, RadarrEvent::DeleteIndexer => self.delete_indexer().await, RadarrEvent::DeleteMovie => self.delete_movie().await, @@ -134,6 +142,7 @@ impl<'a, 'b> Network<'a, 'b> { RadarrEvent::EditCollection => self.edit_collection().await, RadarrEvent::EditIndexer => self.edit_indexer().await, RadarrEvent::EditMovie => self.edit_movie().await, + RadarrEvent::GetBlocklist => self.get_blocklist().await, RadarrEvent::GetCollections => self.get_collections().await, RadarrEvent::GetDownloads => self.get_downloads().await, RadarrEvent::GetIndexers => self.get_indexers().await, @@ -319,6 +328,64 @@ impl<'a, 'b> Network<'a, 'b> { .await; } + async fn clear_blocklist(&mut self) { + info!("Clearing Radarr blocklist"); + + let ids = self + .app + .lock() + .await + .data + .radarr_data + .blocklist + .items + .iter() + .map(|item| item.id) + .collect::>(); + + let request_props = self + .radarr_request_props_from( + RadarrEvent::ClearBlocklist.resource(), + RequestMethod::Delete, + Some(json!({"ids": ids})), + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await; + } + + async fn delete_blocklist_item(&mut self) { + let blocklist_item_id = self + .app + .lock() + .await + .data + .radarr_data + .blocklist + .current_selection() + .id; + + info!("Deleting Radarr blocklist item for item with id: {blocklist_item_id}"); + + let request_props = self + .radarr_request_props_from( + format!( + "{}/{blocklist_item_id}", + RadarrEvent::DeleteBlocklistItem.resource() + ) + .as_str(), + RequestMethod::Delete, + None::<()>, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await; + } + async fn delete_download(&mut self) { let download_id = self .app @@ -794,6 +861,32 @@ impl<'a, 'b> Network<'a, 'b> { .await; } + async fn get_blocklist(&mut self) { + info!("Fetching blocklist"); + + let request_props = self + .radarr_request_props_from( + RadarrEvent::GetBlocklist.resource(), + RequestMethod::Get, + None::<()>, + ) + .await; + + self + .handle_request::<(), BlocklistResponse>(request_props, |blocklist_resp, mut app| { + if !matches!( + app.get_current_route(), + Route::Radarr(ActiveRadarrBlock::BlocklistSortPrompt, _) + ) { + let mut blocklist_vec = blocklist_resp.records; + blocklist_vec.sort_by(|a, b| a.id.cmp(&b.id)); + app.data.radarr_data.blocklist.set_items(blocklist_vec); + app.data.radarr_data.blocklist.apply_sorting_toggle(false); + } + }) + .await; + } + async fn get_collections(&mut self) { info!("Fetching Radarr collections"); diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 126f876..d9dbc7b 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, Language, MediaInfo, MinimumAvailability, Monitor, MovieFile, - Quality, QualityWrapper, Rating, RatingsList, + BlocklistItem, CollectionMovie, IndexerField, Language, MediaInfo, MinimumAvailability, + Monitor, MovieFile, Quality, QualityWrapper, Rating, RatingsList, }; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::stateful_table::SortOption; @@ -186,6 +186,9 @@ mod test { } #[rstest] + #[case(RadarrEvent::ClearBlocklist, "/blocklist/bulk")] + #[case(RadarrEvent::DeleteBlocklistItem, "/blocklist")] + #[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] #[case(RadarrEvent::GetLogs, "/log")] #[case(RadarrEvent::SearchNewMovie, "/movie/lookup")] #[case(RadarrEvent::GetMovieCredits, "/credit")] @@ -1302,6 +1305,271 @@ mod test { ); } + #[rstest] + #[tokio::test] + async fn test_handle_get_blocklist_event(#[values(true, false)] use_custom_sorting: bool) { + let blocklist_json = json!({"records": [{ + "id": 123, + "movieId": 1007, + "sourceTitle": "z movie", + "languages": [{"name": "English"}], + "quality": {"quality": {"name": "HD - 1080p"}}, + "customFormats": [{"name": "English"}], + "date": "2024-02-10T07:28:45Z", + "protocol": "usenet", + "indexer": "DrunkenSlug (Prowlarr)", + "message": "test message", + "movie": { + "id": 1007, + "title": "z movie", + "tmdbId": 1234, + "originalLanguage": {"name": "English"}, + "sizeOnDisk": 3543348019i64, + "status": "Downloaded", + "overview": "Blah blah blah", + "path": "/nfs/movies", + "studio": "21st Century Alex", + "genres": ["cool", "family", "fun"], + "year": 2023, + "monitored": true, + "hasFile": true, + "runtime": 120, + "qualityProfileId": 2222, + "minimumAvailability": "announced", + "certification": "R", + "tags": [1], + "ratings": { + "imdb": {"value": 9.9}, + "tmdb": {"value": 9.9}, + "rottenTomatoes": {"value": 9.9} + }, + }, + }, { + "id": 456, + "movieId": 2001, + "sourceTitle": "A Movie", + "languages": [{"name": "English"}], + "quality": {"quality": {"name": "HD - 1080p"}}, + "customFormats": [{"name": "English"}], + "date": "2024-02-10T07:28:45Z", + "protocol": "usenet", + "indexer": "DrunkenSlug (Prowlarr)", + "message": "test message", + "movie": { + "id": 2001, + "title": "A Movie", + "tmdbId": 1234, + "originalLanguage": {"name": "English"}, + "sizeOnDisk": 3543348019i64, + "status": "Downloaded", + "overview": "Blah blah blah", + "path": "/nfs/movies", + "studio": "21st Century Alex", + "genres": ["cool", "family", "fun"], + "year": 2023, + "monitored": true, + "hasFile": true, + "runtime": 120, + "qualityProfileId": 2222, + "minimumAvailability": "announced", + "certification": "R", + "tags": [1], + "ratings": { + "imdb": {"value": 9.9}, + "tmdb": {"value": 9.9}, + "rottenTomatoes": {"value": 9.9} + }, + }, + }]}); + let mut expected_blocklist = vec![ + BlocklistItem { + id: 123, + movie_id: 1007, + source_title: "z movie".into(), + movie: Movie { + id: 1007, + title: "z movie".into(), + movie_file: None, + collection: None, + ..movie() + }, + ..blocklist_item() + }, + BlocklistItem { + id: 456, + movie_id: 2001, + source_title: "A Movie".into(), + movie: Movie { + id: 2001, + title: "A Movie".into(), + movie_file: None, + collection: None, + ..movie() + }, + ..blocklist_item() + }, + ]; + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(blocklist_json), + None, + RadarrEvent::GetBlocklist.resource(), + ) + .await; + app_arc.lock().await.data.radarr_data.blocklist.sort_asc = true; + if use_custom_sorting { + let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| { + a.source_title + .to_lowercase() + .cmp(&b.source_title.to_lowercase()) + }; + expected_blocklist.sort_by(cmp_fn); + + let blocklist_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app_arc + .lock() + .await + .data + .radarr_data + .blocklist + .sorting(vec![blocklist_sort_option]); + } + let mut network = Network::new(&app_arc, CancellationToken::new()); + + network.handle_radarr_event(RadarrEvent::GetBlocklist).await; + + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.radarr_data.blocklist.items, + expected_blocklist + ); + assert!(app_arc.lock().await.data.radarr_data.blocklist.sort_asc); + } + + #[tokio::test] + async fn test_handle_get_blocklist_event_no_op_when_user_is_selecting_sort_options() { + let blocklist_json = json!({"records": [{ + "id": 123, + "movieId": 1007, + "sourceTitle": "z movie", + "languages": [{"name": "English"}], + "quality": {"quality": {"name": "HD - 1080p"}}, + "customFormats": [{"name": "English"}], + "date": "2024-02-10T07:28:45Z", + "protocol": "usenet", + "indexer": "DrunkenSlug (Prowlarr)", + "message": "test message", + "movie": { + "id": 1007, + "title": "z movie", + "tmdbId": 1234, + "originalLanguage": {"name": "English"}, + "sizeOnDisk": 3543348019i64, + "status": "Downloaded", + "overview": "Blah blah blah", + "path": "/nfs/movies", + "studio": "21st Century Alex", + "genres": ["cool", "family", "fun"], + "year": 2023, + "monitored": true, + "hasFile": true, + "runtime": 120, + "qualityProfileId": 2222, + "minimumAvailability": "announced", + "certification": "R", + "tags": [1], + "ratings": { + "imdb": {"value": 9.9}, + "tmdb": {"value": 9.9}, + "rottenTomatoes": {"value": 9.9} + }, + }, + }, { + "id": 456, + "movieId": 2001, + "sourceTitle": "A Movie", + "languages": [{"name": "English"}], + "quality": {"quality": {"name": "HD - 1080p"}}, + "customFormats": [{"name": "English"}], + "date": "2024-02-10T07:28:45Z", + "protocol": "usenet", + "indexer": "DrunkenSlug (Prowlarr)", + "message": "test message", + "movie": { + "id": 2001, + "title": "A Movie", + "tmdbId": 1234, + "originalLanguage": {"name": "English"}, + "sizeOnDisk": 3543348019i64, + "status": "Downloaded", + "overview": "Blah blah blah", + "path": "/nfs/movies", + "studio": "21st Century Alex", + "genres": ["cool", "family", "fun"], + "year": 2023, + "monitored": true, + "hasFile": true, + "runtime": 120, + "qualityProfileId": 2222, + "minimumAvailability": "announced", + "certification": "R", + "tags": [1], + "ratings": { + "imdb": {"value": 9.9}, + "tmdb": {"value": 9.9}, + "rottenTomatoes": {"value": 9.9} + }, + }, + }]}); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Get, + None, + Some(blocklist_json), + None, + RadarrEvent::GetBlocklist.resource(), + ) + .await; + app_arc.lock().await.data.radarr_data.blocklist.sort_asc = true; + app_arc + .lock() + .await + .push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into()); + let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| { + a.source_title + .to_lowercase() + .cmp(&b.source_title.to_lowercase()) + }; + let blocklist_sort_option = SortOption { + name: "Source Title", + cmp_fn: Some(cmp_fn), + }; + app_arc + .lock() + .await + .data + .radarr_data + .blocklist + .sorting(vec![blocklist_sort_option]); + let mut network = Network::new(&app_arc, CancellationToken::new()); + + network.handle_radarr_event(RadarrEvent::GetBlocklist).await; + + async_server.assert_async().await; + assert!(app_arc + .lock() + .await + .data + .radarr_data + .blocklist + .items + .is_empty()); + assert!(app_arc.lock().await.data.radarr_data.blocklist.sort_asc); + } + #[rstest] #[tokio::test] async fn test_handle_get_collections_event(#[values(true, false)] use_custom_sorting: bool) { @@ -2154,6 +2422,68 @@ mod test { assert!(!app_arc.lock().await.data.radarr_data.add_list_exclusion); } + #[tokio::test] + async fn test_handle_clear_blocklist_event() { + let blocklist_items = vec![ + BlocklistItem { + id: 1, + ..blocklist_item() + }, + BlocklistItem { + id: 2, + ..blocklist_item() + }, + BlocklistItem { + id: 3, + ..blocklist_item() + }, + ]; + let expected_request_json = json!({ "ids": [1, 2, 3]}); + let (async_server, app_arc, _server) = mock_radarr_api( + RequestMethod::Delete, + Some(expected_request_json), + None, + None, + RadarrEvent::ClearBlocklist.resource(), + ) + .await; + app_arc + .lock() + .await + .data + .radarr_data + .blocklist + .set_items(blocklist_items); + let mut network = Network::new(&app_arc, CancellationToken::new()); + + network + .handle_radarr_event(RadarrEvent::ClearBlocklist) + .await; + + async_server.assert_async().await; + } + + #[tokio::test] + async fn test_handle_delete_blocklist_item_event() { + let resource = format!("{}/1", RadarrEvent::DeleteBlocklistItem.resource()); + let (async_server, app_arc, _server) = + mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await; + app_arc + .lock() + .await + .data + .radarr_data + .blocklist + .set_items(vec![blocklist_item()]); + let mut network = Network::new(&app_arc, CancellationToken::new()); + + network + .handle_radarr_event(RadarrEvent::DeleteBlocklistItem) + .await; + + async_server.assert_async().await; + } + #[tokio::test] async fn test_handle_delete_download_event() { let resource = format!("{}/1", RadarrEvent::DeleteDownload.resource()); @@ -3311,6 +3641,22 @@ mod test { } } + fn blocklist_item() -> BlocklistItem { + BlocklistItem { + id: 1, + movie_id: 1, + source_title: "z movie".to_owned(), + languages: vec![language()], + quality: quality_wrapper(), + custom_formats: Some(vec![language()]), + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + protocol: "usenet".to_owned(), + indexer: "DrunkenSlug (Prowlarr)".to_owned(), + message: "test message".to_owned(), + movie: movie(), + } + } + fn collection() -> Collection { Collection { id: 123, diff --git a/src/ui/radarr_ui/blocklist/blocklist_ui_tests.rs b/src/ui/radarr_ui/blocklist/blocklist_ui_tests.rs new file mode 100644 index 0000000..7609341 --- /dev/null +++ b/src/ui/radarr_ui/blocklist/blocklist_ui_tests.rs @@ -0,0 +1,18 @@ +#[cfg(test)] +mod tests { + use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; + use crate::ui::radarr_ui::blocklist::BlocklistUi; + use crate::ui::DrawUi; + use strum::IntoEnumIterator; + + #[test] + fn test_blocklist_ui_accepts() { + ActiveRadarrBlock::iter().for_each(|active_radarr_block| { + if BLOCKLIST_BLOCKS.contains(&active_radarr_block) { + assert!(BlocklistUi::accepts(active_radarr_block.into())); + } else { + assert!(!BlocklistUi::accepts(active_radarr_block.into())); + } + }); + } +} diff --git a/src/ui/radarr_ui/blocklist/mod.rs b/src/ui/radarr_ui/blocklist/mod.rs new file mode 100644 index 0000000..714780b --- /dev/null +++ b/src/ui/radarr_ui/blocklist/mod.rs @@ -0,0 +1,201 @@ +use crate::app::App; +use crate::models::radarr_models::BlocklistItem; +use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS}; +use crate::models::Route; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::message::Message; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::DrawUi; +use ratatui::layout::{Alignment, Constraint, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::{Line, Text}; +use ratatui::widgets::{Cell, Row}; +use ratatui::Frame; + +#[cfg(test)] +#[path = "blocklist_ui_tests.rs"] +mod blocklist_ui_tests; + +pub(super) struct BlocklistUi; + +impl DrawUi for BlocklistUi { + fn accepts(route: Route) -> bool { + if let Route::Radarr(active_radarr_block, _) = route { + return BLOCKLIST_BLOCKS.contains(&active_radarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + match active_radarr_block { + ActiveRadarrBlock::Blocklist | ActiveRadarrBlock::BlocklistSortPrompt => { + draw_blocklist_table(f, app, area) + } + ActiveRadarrBlock::BlocklistItemDetails => { + draw_blocklist_table(f, app, area); + draw_blocklist_item_details_popup(f, app); + } + ActiveRadarrBlock::DeleteBlocklistItemPrompt => { + let prompt = format!( + "Do you want to remove this item from your blocklist: \n{}?", + app + .data + .radarr_data + .blocklist + .current_selection() + .source_title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Remove Item from Blocklist") + .prompt(&prompt) + .yes_no_value(app.data.radarr_data.prompt_confirm); + + draw_blocklist_table(f, app, area); + f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size()); + } + ActiveRadarrBlock::BlocklistClearAllItemsPrompt => { + let confirmation_prompt = ConfirmationPrompt::new() + .title("Clear Blocklist") + .prompt("Do you want to clear your blocklist?") + .yes_no_value(app.data.radarr_data.prompt_confirm); + + draw_blocklist_table(f, app, area); + f.render_widget( + Popup::new(confirmation_prompt).size(Size::SmallPrompt), + f.size(), + ); + } + _ => (), + } + } + } +} + +fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { + let current_selection = if app.data.radarr_data.blocklist.items.is_empty() { + BlocklistItem::default() + } else { + app.data.radarr_data.blocklist.current_selection().clone() + }; + let blocklist_table_footer = app + .data + .radarr_data + .main_tabs + .get_active_tab_contextual_help(); + + let blocklist_row_mapping = |blocklist_item: &BlocklistItem| { + let BlocklistItem { + source_title, + languages, + quality, + custom_formats, + date, + movie, + .. + } = blocklist_item; + + movie.title.scroll_left_or_reset( + get_width_from_percentage(area, 20), + current_selection == *blocklist_item, + app.tick_count % app.ticks_until_scroll == 0, + ); + + let languages_string = languages + .iter() + .map(|lang| lang.name.to_owned()) + .collect::>() + .join(", "); + let custom_formats_string = if let Some(formats) = custom_formats.as_ref() { + formats + .iter() + .map(|cf| cf.name.to_owned()) + .collect::>() + .join(", ") + } else { + "".to_owned() + }; + + Row::new(vec![ + Cell::from(movie.title.to_string()), + Cell::from(source_title.to_owned()), + Cell::from(languages_string), + Cell::from(quality.quality.name.to_owned()), + Cell::from(custom_formats_string), + Cell::from(date.to_string()), + ]) + .primary() + }; + let blocklist_table = ManagarrTable::new( + Some(&mut app.data.radarr_data.blocklist), + blocklist_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .footer(blocklist_table_footer) + .sorting(active_radarr_block == ActiveRadarrBlock::BlocklistSortPrompt) + .headers([ + "Movie Title", + "Source Title", + "Languages", + "Quality", + "Formats", + "Date", + ]) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(35), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(15), + ]); + + f.render_widget(blocklist_table, area); + } +} + +fn draw_blocklist_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { + let current_selection = if app.data.radarr_data.blocklist.items.is_empty() { + BlocklistItem::default() + } else { + app.data.radarr_data.blocklist.current_selection().clone() + }; + let BlocklistItem { + source_title, + protocol, + indexer, + message, + .. + } = current_selection; + let text = Text::from(vec![ + Line::from(vec![ + "Name: ".bold().secondary(), + source_title.to_owned().secondary(), + ]), + Line::from(vec![ + "Protocol: ".bold().secondary(), + protocol.to_owned().secondary(), + ]), + Line::from(vec![ + "Indexer: ".bold().secondary(), + indexer.to_owned().secondary(), + ]), + Line::from(vec![ + "Message: ".bold().secondary(), + message.to_owned().secondary(), + ]), + ]); + + let message = Message::new(text) + .title("Details") + .style(Style::new().secondary()) + .alignment(Alignment::Left); + + f.render_widget(Popup::new(message).size(Size::NarrowMessage), f.size()); +} diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index 68095f3..ad481fd 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -13,6 +13,7 @@ use crate::models::radarr_models::{DiskSpace, DownloadRecord, Movie, RootFolder} use crate::models::servarr_data::radarr::radarr_data::RadarrData; use crate::models::Route; use crate::ui::draw_tabs; +use crate::ui::radarr_ui::blocklist::BlocklistUi; use crate::ui::radarr_ui::collections::CollectionsUi; use crate::ui::radarr_ui::downloads::DownloadsUi; use crate::ui::radarr_ui::indexers::IndexersUi; @@ -27,6 +28,7 @@ use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::DrawUi; use crate::utils::convert_to_gb; +mod blocklist; mod collections; mod downloads; mod indexers; @@ -57,6 +59,7 @@ impl DrawUi for RadarrUi { _ if IndexersUi::accepts(route) => IndexersUi::draw(f, app, content_area), _ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area), _ if SystemUi::accepts(route) => SystemUi::draw(f, app, content_area), + _ if BlocklistUi::accepts(route) => BlocklistUi::draw(f, app, content_area), _ => (), } } diff --git a/src/ui/widgets/message.rs b/src/ui/widgets/message.rs index de880ea..7543430 100644 --- a/src/ui/widgets/message.rs +++ b/src/ui/widgets/message.rs @@ -14,6 +14,7 @@ pub struct Message<'a> { text: Text<'a>, title: &'a str, style: Style, + alignment: Alignment, } impl<'a> Message<'a> { @@ -25,6 +26,7 @@ impl<'a> Message<'a> { text: message.into(), title: "Error", style: Style::new().failure().bold(), + alignment: Alignment::Center, } } @@ -38,10 +40,15 @@ impl<'a> Message<'a> { self } + pub fn alignment(mut self, alignment: Alignment) -> Self { + self.alignment = alignment; + self + } + fn render_message(self, area: Rect, buf: &mut Buffer) { Paragraph::new(self.text) .style(self.style) - .alignment(Alignment::Center) + .alignment(self.alignment) .block(title_block_centered(self.title).style(self.style)) .wrap(Wrap { trim: true }) .render(area, buf); diff --git a/src/ui/widgets/message_tests.rs b/src/ui/widgets/message_tests.rs index efa875a..cf72796 100644 --- a/src/ui/widgets/message_tests.rs +++ b/src/ui/widgets/message_tests.rs @@ -3,6 +3,7 @@ mod tests { use crate::ui::styles::ManagarrStyle; use crate::ui::widgets::message::Message; use pretty_assertions::{assert_eq, assert_str_eq}; + use ratatui::layout::Alignment; use ratatui::style::{Style, Stylize}; use ratatui::text::Text; @@ -15,6 +16,7 @@ mod tests { assert_eq!(message.text, Text::from(test_message)); assert_str_eq!(message.title, "Error"); assert_eq!(message.style, Style::new().failure().bold()); + assert_eq!(message.alignment, Alignment::Center); } #[test] @@ -27,6 +29,7 @@ mod tests { assert_str_eq!(message.title, title); assert_eq!(message.text, Text::from(test_message)); assert_eq!(message.style, Style::new().failure().bold()); + assert_eq!(message.alignment, Alignment::Center); } #[test] @@ -39,5 +42,18 @@ mod tests { assert_eq!(message.style, style); assert_eq!(message.text, Text::from(test_message)); assert_str_eq!(message.title, "Error"); + assert_eq!(message.alignment, Alignment::Center); + } + + #[test] + fn test_message_alignment() { + let test_message = "This is a message"; + + let message = Message::new(test_message).alignment(Alignment::Left); + + assert_eq!(message.alignment, Alignment::Left); + assert_eq!(message.text, Text::from(test_message)); + assert_str_eq!(message.title, "Error"); + assert_eq!(message.style, Style::new().failure().bold()); } } diff --git a/src/ui/widgets/popup.rs b/src/ui/widgets/popup.rs index c4ac287..563dadc 100644 --- a/src/ui/widgets/popup.rs +++ b/src/ui/widgets/popup.rs @@ -10,9 +10,11 @@ use ratatui::widgets::{Block, Clear, Paragraph, Widget}; mod popup_tests; pub enum Size { + SmallPrompt, Prompt, LargePrompt, Message, + NarrowMessage, LargeMessage, InputBox, Dropdown, @@ -24,9 +26,11 @@ pub enum Size { impl Size { pub fn to_percent(&self) -> (u16, u16) { match self { + Size::SmallPrompt => (20, 20), Size::Prompt => (35, 35), Size::LargePrompt => (70, 45), Size::Message => (25, 8), + Size::NarrowMessage => (50, 20), Size::LargeMessage => (25, 25), Size::InputBox => (30, 13), Size::Dropdown => (20, 30), diff --git a/src/ui/widgets/popup_tests.rs b/src/ui/widgets/popup_tests.rs index 4d4223d..9da904f 100644 --- a/src/ui/widgets/popup_tests.rs +++ b/src/ui/widgets/popup_tests.rs @@ -6,9 +6,11 @@ mod tests { #[test] fn test_dimensions_to_percent() { + assert_eq!(Size::SmallPrompt.to_percent(), (20, 20)); assert_eq!(Size::Prompt.to_percent(), (35, 35)); assert_eq!(Size::LargePrompt.to_percent(), (70, 45)); assert_eq!(Size::Message.to_percent(), (25, 8)); + assert_eq!(Size::NarrowMessage.to_percent(), (50, 20)); assert_eq!(Size::LargeMessage.to_percent(), (25, 25)); assert_eq!(Size::InputBox.to_percent(), (30, 13)); assert_eq!(Size::Dropdown.to_percent(), (20, 30));