diff --git a/README.md b/README.md index 145512e..56e687b 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Key: | 🕒 | ✅ | Search your library | | 🕒 | ✅ | Add series to your library | | 🕒 | ✅ | Delete series, downloads, indexers, root folders, and episode files | -| 🕒 | ✅ | Mark history events as failed | +| 🚫 | ✅ | Mark history events as failed | | 🕒 | ✅ | Trigger automatic searches for series, seasons, or episodes | | 🕒 | ✅ | Trigger refresh and disk scan for series and downloads | | 🕒 | ✅ | Manually search for series, seasons, or episodes | diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 9b6a30b..563d7b5 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -69,7 +69,7 @@ impl<'a> App<'a> { } ActiveSonarrBlock::History => { self - .dispatch_network_event(SonarrEvent::GetHistory(None).into()) + .dispatch_network_event(SonarrEvent::GetHistory(Some(10000)).into()) .await; } ActiveSonarrBlock::RootFolders => { diff --git a/src/app/sonarr/sonarr_context_clues.rs b/src/app/sonarr/sonarr_context_clues.rs index ee311ec..9aacf69 100644 --- a/src/app/sonarr/sonarr_context_clues.rs +++ b/src/app/sonarr/sonarr_context_clues.rs @@ -25,9 +25,8 @@ pub static SERIES_CONTEXT_CLUES: [ContextClue; 10] = [ (DEFAULT_KEYBINDINGS.esc, "cancel filter"), ]; -pub static HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [ +pub static HISTORY_CONTEXT_CLUES: [ContextClue; 5] = [ (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), - (DEFAULT_KEYBINDINGS.delete, "mark as failed"), (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), ( diff --git a/src/app/sonarr/sonarr_context_clues_tests.rs b/src/app/sonarr/sonarr_context_clues_tests.rs index d25992c..c72b0b2 100644 --- a/src/app/sonarr/sonarr_context_clues_tests.rs +++ b/src/app/sonarr/sonarr_context_clues_tests.rs @@ -96,11 +96,6 @@ mod tests { let (key_binding, description) = history_context_clues_iter.next().unwrap(); - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete); - assert_str_eq!(*description, "mark as failed"); - - let (key_binding, description) = history_context_clues_iter.next().unwrap(); - assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search); assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc); diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index a02c217..123212b 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -179,7 +179,7 @@ mod tests { assert!(app.is_loading); assert_eq!( sync_network_rx.recv().await.unwrap(), - SonarrEvent::GetHistory(None).into() + SonarrEvent::GetHistory(Some(10000)).into() ); assert!(!app.data.sonarr_data.prompt_confirm); assert_eq!(app.tick_count, 0); diff --git a/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs index 6bf173b..026f842 100644 --- a/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs +++ b/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs @@ -475,8 +475,6 @@ mod tests { use pretty_assertions::assert_eq; use rstest::rstest; - use crate::handlers::sonarr_handlers::downloads::DownloadsHandler; - use super::*; const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; @@ -547,7 +545,7 @@ mod tests { app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); app.push_navigation_stack(ActiveSonarrBlock::Blocklist.into()); - DownloadsHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::Blocklist, None).handle(); + BlocklistHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::Blocklist, None).handle(); assert_eq!(app.get_current_route(), ActiveSonarrBlock::Blocklist.into()); assert!(app.error.text.is_empty()); diff --git a/src/handlers/sonarr_handlers/history/history_handler_tests.rs b/src/handlers/sonarr_handlers/history/history_handler_tests.rs new file mode 100644 index 0000000..e981ec6 --- /dev/null +++ b/src/handlers/sonarr_handlers/history/history_handler_tests.rs @@ -0,0 +1,1396 @@ +#[cfg(test)] +mod tests { + use core::sync::atomic::Ordering::SeqCst; + use std::cmp::Ordering; + + use chrono::DateTime; + use pretty_assertions::{assert_eq, assert_str_eq}; + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::history::{history_sorting_options, HistoryHandler}; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; + use crate::models::servarr_models::{Language, Quality, QualityWrapper}; + use crate::models::sonarr_models::SonarrHistoryItem; + use crate::models::stateful_table::SortOption; + use crate::models::HorizontallyScrollableText; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + + use crate::models::sonarr_models::SonarrHistoryItem; + use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; + + use super::*; + + test_iterable_scroll!( + test_history_scroll, + HistoryHandler, + sonarr_data, + history, + simple_stateful_iterable_vec!(SonarrHistoryItem, HorizontallyScrollableText, source_title), + ActiveSonarrBlock::History, + None, + source_title, + to_string + ); + + #[rstest] + fn test_history_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.is_loading = true; + app + .data + .sonarr_data + .history + .set_items(simple_stateful_iterable_vec!( + SonarrHistoryItem, + HorizontallyScrollableText, + source_title + )); + + HistoryHandler::with(key, &mut app, ActiveSonarrBlock::History, None).handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .history + .current_selection() + .source_title + .to_string(), + "Test 1" + ); + + HistoryHandler::with(key, &mut app, ActiveSonarrBlock::History, None).handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .history + .current_selection() + .source_title + .to_string(), + "Test 1" + ); + } + + #[rstest] + fn test_history_sort_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let history_field_vec = sort_options(); + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.data.sonarr_data.history.sorting(sort_options()); + + if key == Key::Up { + for i in (0..history_field_vec.len()).rev() { + HistoryHandler::with(key, &mut app, ActiveSonarrBlock::HistorySortPrompt, None).handle(); + + assert_eq!( + app + .data + .sonarr_data + .history + .sort + .as_ref() + .unwrap() + .current_selection(), + &history_field_vec[i] + ); + } + } else { + for i in 0..history_field_vec.len() { + HistoryHandler::with(key, &mut app, ActiveSonarrBlock::HistorySortPrompt, None).handle(); + + assert_eq!( + app + .data + .sonarr_data + .history + .sort + .as_ref() + .unwrap() + .current_selection(), + &history_field_vec[(i + 1) % history_field_vec.len()] + ); + } + } + } + } + + mod test_handle_home_end { + use pretty_assertions::{assert_eq, assert_str_eq}; + + use crate::models::sonarr_models::SonarrHistoryItem; + use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; + + use super::*; + + test_iterable_home_and_end!( + test_history_home_and_end, + HistoryHandler, + sonarr_data, + history, + extended_stateful_iterable_vec!(SonarrHistoryItem, HorizontallyScrollableText, source_title), + ActiveSonarrBlock::History, + None, + source_title, + to_string + ); + + #[test] + fn test_history_home_and_end_no_op_when_not_ready() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.is_loading = true; + app + .data + .sonarr_data + .history + .set_items(extended_stateful_iterable_vec!( + SonarrHistoryItem, + HorizontallyScrollableText, + source_title + )); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .history + .current_selection() + .source_title + .to_string(), + "Test 1" + ); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .history + .current_selection() + .source_title + .to_string(), + "Test 1" + ); + } + + #[test] + fn test_history_search_box_home_end_keys() { + let mut app = App::default(); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.history.search = Some("Test".into()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SearchHistory, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .history + .search + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 4 + ); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SearchHistory, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .history + .search + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 0 + ); + } + + #[test] + fn test_history_filter_box_home_end_keys() { + let mut app = App::default(); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.history.filter = Some("Test".into()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::FilterHistory, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .history + .filter + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 4 + ); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::FilterHistory, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .history + .filter + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 0 + ); + } + + #[test] + fn test_history_sort_home_end() { + let history_field_vec = sort_options(); + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.data.sonarr_data.history.sorting(sort_options()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::HistorySortPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .history + .sort + .as_ref() + .unwrap() + .current_selection(), + &history_field_vec[history_field_vec.len() - 1] + ); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::HistorySortPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .history + .sort + .as_ref() + .unwrap() + .current_selection(), + &history_field_vec[0] + ); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_history_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(3); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::Blocklist.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Blocklist.into()); + } + + #[rstest] + fn test_history_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(3); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + + #[test] + fn test_history_search_box_left_right_keys() { + let mut app = App::default(); + app.data.sonarr_data.history.search = Some("Test".into()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::SearchHistory, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .history + .search + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 1 + ); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::SearchHistory, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .history + .search + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 0 + ); + } + + #[test] + fn test_history_filter_box_left_right_keys() { + let mut app = App::default(); + app.data.sonarr_data.history.filter = Some("Test".into()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::FilterHistory, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .history + .filter + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 1 + ); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::FilterHistory, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .history + .filter + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::extended_stateful_iterable_vec; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_history_submit() { + let mut app = App::default(); + app.data.sonarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + + HistoryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::History, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::HistoryItemDetails.into() + ); + } + + #[test] + fn test_history_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.data.sonarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + + HistoryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::History, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + } + + #[test] + fn test_search_history_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(ActiveSonarrBlock::SearchHistory.into()); + app + .data + .sonarr_data + .history + .set_items(extended_stateful_iterable_vec!( + SonarrHistoryItem, + HorizontallyScrollableText, + source_title + )); + app.data.sonarr_data.history.search = Some("Test 2".into()); + + HistoryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchHistory, None).handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .history + .current_selection() + .source_title + .text, + "Test 2" + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + } + + #[test] + fn test_search_history_submit_error_on_no_search_hits() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(ActiveSonarrBlock::SearchHistory.into()); + app + .data + .sonarr_data + .history + .set_items(extended_stateful_iterable_vec!( + SonarrHistoryItem, + HorizontallyScrollableText, + source_title + )); + app.data.sonarr_data.history.search = Some("Test 5".into()); + + HistoryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchHistory, None).handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .history + .current_selection() + .source_title + .text, + "Test 1" + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SearchHistoryError.into() + ); + } + + #[test] + fn test_search_filtered_history_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(ActiveSonarrBlock::SearchHistory.into()); + app + .data + .sonarr_data + .history + .set_filtered_items(extended_stateful_iterable_vec!( + SonarrHistoryItem, + HorizontallyScrollableText, + source_title + )); + app.data.sonarr_data.history.search = Some("Test 2".into()); + + HistoryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchHistory, None).handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .history + .current_selection() + .source_title + .text, + "Test 2" + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + } + + #[test] + fn test_filter_history_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(ActiveSonarrBlock::FilterHistory.into()); + app + .data + .sonarr_data + .history + .set_items(extended_stateful_iterable_vec!( + SonarrHistoryItem, + HorizontallyScrollableText, + source_title + )); + app.data.sonarr_data.history.filter = Some("Test".into()); + + HistoryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::FilterHistory, None).handle(); + + assert!(app.data.sonarr_data.history.filtered_items.is_some()); + assert!(!app.should_ignore_quit_key); + assert_eq!( + app + .data + .sonarr_data + .history + .filtered_items + .as_ref() + .unwrap() + .len(), + 3 + ); + assert_str_eq!( + app + .data + .sonarr_data + .history + .current_selection() + .source_title + .text, + "Test 1" + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + } + + #[test] + fn test_filter_history_submit_error_on_no_filter_matches() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(ActiveSonarrBlock::FilterHistory.into()); + app + .data + .sonarr_data + .history + .set_items(extended_stateful_iterable_vec!( + SonarrHistoryItem, + HorizontallyScrollableText, + source_title + )); + app.data.sonarr_data.history.filter = Some("Test 5".into()); + + HistoryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::FilterHistory, None).handle(); + + assert!(!app.should_ignore_quit_key); + assert!(app.data.sonarr_data.history.filtered_items.is_none()); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::FilterHistoryError.into() + ); + } + + #[test] + fn test_history_sort_prompt_submit() { + let mut app = App::default(); + app.data.sonarr_data.history.sort_asc = true; + app.data.sonarr_data.history.sorting(sort_options()); + app.data.sonarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(ActiveSonarrBlock::HistorySortPrompt.into()); + + let mut expected_vec = history_vec(); + expected_vec.sort_by(|a, b| a.id.cmp(&b.id)); + expected_vec.reverse(); + + HistoryHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::HistorySortPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + assert_eq!(app.data.sonarr_data.history.items, expected_vec); + } + } + + mod test_handle_esc { + use pretty_assertions::assert_eq; + use ratatui::widgets::TableState; + use rstest::rstest; + + use crate::models::{ + servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data, + stateful_table::StatefulTable, + }; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_search_history_block_esc( + #[values( + ActiveSonarrBlock::SearchHistory, + ActiveSonarrBlock::SearchHistoryError + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(active_sonarr_block.into()); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.history.search = Some("Test".into()); + + HistoryHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + assert!(!app.should_ignore_quit_key); + assert_eq!(app.data.sonarr_data.history.search, None); + } + + #[rstest] + fn test_filter_history_block_esc( + #[values( + ActiveSonarrBlock::FilterHistory, + ActiveSonarrBlock::FilterHistoryError + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(active_sonarr_block.into()); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.history = StatefulTable { + filter: Some("Test".into()), + filtered_items: Some(Vec::new()), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; + + HistoryHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + assert!(!app.should_ignore_quit_key); + assert_eq!(app.data.sonarr_data.history.filter, None); + assert_eq!(app.data.sonarr_data.history.filtered_items, None); + assert_eq!(app.data.sonarr_data.history.filtered_state, None); + } + + #[test] + fn test_esc_history_item_details() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(ActiveSonarrBlock::HistoryItemDetails.into()); + + HistoryHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::HistoryItemDetails, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + } + + #[test] + fn test_history_sort_prompt_block_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(ActiveSonarrBlock::HistorySortPrompt.into()); + + HistoryHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::HistorySortPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + + HistoryHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::History, None).handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + assert!(app.error.text.is_empty()); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + + use super::*; + + #[test] + fn test_search_history_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.search.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SearchHistory.into() + ); + assert!(app.should_ignore_quit_key); + assert_eq!( + app.data.sonarr_data.history.search, + Some(HorizontallyScrollableText::default()) + ); + } + + #[test] + fn test_search_history_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.search.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + assert!(!app.should_ignore_quit_key); + assert_eq!(app.data.sonarr_data.history.search, None); + } + + #[test] + fn test_filter_history_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.filter.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::FilterHistory.into() + ); + assert!(app.should_ignore_quit_key); + assert!(app.data.sonarr_data.history.filter.is_some()); + } + + #[test] + fn test_filter_history_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.filter.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + assert!(!app.should_ignore_quit_key); + assert!(app.data.sonarr_data.history.filter.is_none()); + } + + #[test] + fn test_filter_history_key_resets_previous_filter() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.data.sonarr_data = create_test_sonarr_data(); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.history.filter = Some("Test".into()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.filter.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::FilterHistory.into() + ); + assert!(app.should_ignore_quit_key); + assert_eq!( + app.data.sonarr_data.history.filter, + Some(HorizontallyScrollableText::default()) + ); + assert!(app.data.sonarr_data.history.filtered_items.is_none()); + assert!(app.data.sonarr_data.history.filtered_state.is_none()); + } + + #[test] + fn test_refresh_history_key() { + let mut app = App::default(); + app.data.sonarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_history_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.data.sonarr_data.history.set_items(history_vec()); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + assert!(!app.should_refresh); + } + + #[test] + fn test_search_history_box_backspace_key() { + let mut app = App::default(); + app.data.sonarr_data.history.search = Some("Test".into()); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::SearchHistory, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.history.search.as_ref().unwrap().text, + "Tes" + ); + } + + #[test] + fn test_filter_history_box_backspace_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.history.filter = Some("Test".into()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::FilterHistory, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.history.filter.as_ref().unwrap().text, + "Tes" + ); + } + + #[test] + fn test_search_history_box_char_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.history.search = Some(HorizontallyScrollableText::default()); + + HistoryHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::SearchHistory, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.history.search.as_ref().unwrap().text, + "h" + ); + } + + #[test] + fn test_filter_history_box_char_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.history.filter = Some(HorizontallyScrollableText::default()); + + HistoryHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::FilterHistory, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.history.filter.as_ref().unwrap().text, + "h" + ); + } + + #[test] + fn test_sort_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.data.sonarr_data.history.set_items(history_vec()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.sort.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::HistorySortPrompt.into() + ); + assert_eq!( + app.data.sonarr_data.history.sort.as_ref().unwrap().items, + history_sorting_options() + ); + assert!(!app.data.sonarr_data.history.sort_asc); + } + + #[test] + fn test_sort_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.data.sonarr_data.history.set_items(history_vec()); + + HistoryHandler::with( + DEFAULT_KEYBINDINGS.sort.key, + &mut app, + ActiveSonarrBlock::History, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + assert!(app.data.sonarr_data.history.sort.is_none()); + assert!(!app.data.sonarr_data.history.sort_asc); + } + } + + #[test] + fn test_history_sorting_options_source_title() { + let expected_cmp_fn: fn(&SonarrHistoryItem, &SonarrHistoryItem) -> Ordering = |a, b| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[0].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Source Title"); + } + + #[test] + fn test_history_sorting_options_event_type() { + let expected_cmp_fn: fn(&SonarrHistoryItem, &SonarrHistoryItem) -> Ordering = |a, b| { + a.event_type + .to_lowercase() + .cmp(&b.event_type.to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[1].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Event Type"); + } + + #[test] + fn test_history_sorting_options_language() { + let expected_cmp_fn: fn(&SonarrHistoryItem, &SonarrHistoryItem) -> Ordering = |a, b| { + a.language + .name + .to_lowercase() + .cmp(&b.language.name.to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[2].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Language"); + } + + #[test] + fn test_history_sorting_options_quality() { + let expected_cmp_fn: fn(&SonarrHistoryItem, &SonarrHistoryItem) -> Ordering = |a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }; + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[3].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Quality"); + } + + #[test] + fn test_history_sorting_options_date() { + let expected_cmp_fn: fn(&SonarrHistoryItem, &SonarrHistoryItem) -> Ordering = + |a, b| a.date.cmp(&b.date); + let mut expected_history_vec = history_vec(); + expected_history_vec.sort_by(expected_cmp_fn); + + let sort_option = history_sorting_options()[4].clone(); + let mut sorted_history_vec = history_vec(); + sorted_history_vec.sort_by(sort_option.cmp_fn.unwrap()); + + assert_eq!(sorted_history_vec, expected_history_vec); + assert_str_eq!(sort_option.name, "Date"); + } + + #[test] + fn test_history_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if HISTORY_BLOCKS.contains(&active_sonarr_block) { + assert!(HistoryHandler::accepts(active_sonarr_block)); + } else { + assert!(!HistoryHandler::accepts(active_sonarr_block)); + } + }) + } + + #[test] + fn test_history_handler_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.is_loading = true; + + let handler = HistoryHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::History, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_history_handler_not_ready_when_history_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.is_loading = false; + + let handler = HistoryHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::History, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_history_handler_ready_when_not_loading_and_history_is_not_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::History.into()); + app.is_loading = false; + app + .data + .sonarr_data + .history + .set_items(vec![SonarrHistoryItem::default()]); + + let handler = HistoryHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::History, + None, + ); + + assert!(handler.is_ready()); + } + + fn history_vec() -> Vec { + vec![ + SonarrHistoryItem { + id: 3, + source_title: "test 1".into(), + event_type: "grabbed".to_owned(), + language: Language { + id: 1, + name: "telgu".to_owned(), + }, + quality: QualityWrapper { + quality: Quality { + name: "HD - 1080p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()), + ..SonarrHistoryItem::default() + }, + SonarrHistoryItem { + id: 2, + source_title: "test 2".into(), + event_type: "downloadFolderImported".to_owned(), + language: Language { + id: 3, + name: "chinese".to_owned(), + }, + quality: QualityWrapper { + quality: Quality { + name: "SD - 720p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + ..SonarrHistoryItem::default() + }, + SonarrHistoryItem { + id: 1, + source_title: "test 3".into(), + event_type: "episodeFileDeleted".to_owned(), + language: Language { + id: 1, + name: "english".to_owned(), + }, + quality: QualityWrapper { + quality: Quality { + name: "HD - 1080p".to_owned(), + }, + }, + date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()), + ..SonarrHistoryItem::default() + }, + ] + } + + fn sort_options() -> Vec> { + vec![SortOption { + name: "Test 1", + cmp_fn: Some(|a, b| { + b.source_title + .text + .to_lowercase() + .cmp(&a.source_title.text.to_lowercase()) + }), + }] + } +} diff --git a/src/handlers/sonarr_handlers/history/mod.rs b/src/handlers/sonarr_handlers/history/mod.rs new file mode 100644 index 0000000..7b7069c --- /dev/null +++ b/src/handlers/sonarr_handlers/history/mod.rs @@ -0,0 +1,354 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::{handle_clear_errors, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; +use crate::models::sonarr_models::SonarrHistoryItem; +use crate::models::stateful_table::SortOption; +use crate::models::{HorizontallyScrollableText, Scrollable}; +use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; + +#[cfg(test)] +#[path = "history_handler_tests.rs"] +mod history_handler_tests; + +pub(super) struct HistoryHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, 'b> { + fn accepts(active_block: ActiveSonarrBlock) -> bool { + HISTORY_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + context: Option, + ) -> Self { + HistoryHandler { + key, + app, + active_sonarr_block: active_block, + _context: context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.sonarr_data.history.is_empty() + } + + fn handle_scroll_up(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::History => self.app.data.sonarr_data.history.scroll_up(), + ActiveSonarrBlock::HistorySortPrompt => self + .app + .data + .sonarr_data + .history + .sort + .as_mut() + .unwrap() + .scroll_up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::History => self.app.data.sonarr_data.history.scroll_down(), + ActiveSonarrBlock::HistorySortPrompt => self + .app + .data + .sonarr_data + .history + .sort + .as_mut() + .unwrap() + .scroll_down(), + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::History => self.app.data.sonarr_data.history.scroll_to_top(), + ActiveSonarrBlock::SearchHistory => { + self + .app + .data + .sonarr_data + .history + .search + .as_mut() + .unwrap() + .scroll_home(); + } + ActiveSonarrBlock::FilterHistory => { + self + .app + .data + .sonarr_data + .history + .filter + .as_mut() + .unwrap() + .scroll_home(); + } + ActiveSonarrBlock::HistorySortPrompt => self + .app + .data + .sonarr_data + .history + .sort + .as_mut() + .unwrap() + .scroll_to_top(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::History => self.app.data.sonarr_data.history.scroll_to_bottom(), + ActiveSonarrBlock::SearchHistory => self + .app + .data + .sonarr_data + .history + .search + .as_mut() + .unwrap() + .reset_offset(), + ActiveSonarrBlock::FilterHistory => self + .app + .data + .sonarr_data + .history + .filter + .as_mut() + .unwrap() + .reset_offset(), + ActiveSonarrBlock::HistorySortPrompt => self + .app + .data + .sonarr_data + .history + .sort + .as_mut() + .unwrap() + .scroll_to_bottom(), + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::History => handle_change_tab_left_right_keys(self.app, self.key), + ActiveSonarrBlock::SearchHistory => { + handle_text_box_left_right_keys!( + self, + self.key, + self.app.data.sonarr_data.history.search.as_mut().unwrap() + ) + } + ActiveSonarrBlock::FilterHistory => { + handle_text_box_left_right_keys!( + self, + self.key, + self.app.data.sonarr_data.history.filter.as_mut().unwrap() + ) + } + _ => {} + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SearchHistory => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + + if self.app.data.sonarr_data.history.search.is_some() { + let has_match = self + .app + .data + .sonarr_data + .history + .apply_search(|history| &history.source_title.text); + + if !has_match { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SearchHistoryError.into()); + } + } + } + ActiveSonarrBlock::FilterHistory => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + + if self.app.data.sonarr_data.history.filter.is_some() { + let has_matches = self + .app + .data + .sonarr_data + .history + .apply_filter(|history| &history.source_title.text); + + if !has_matches { + self + .app + .push_navigation_stack(ActiveSonarrBlock::FilterHistoryError.into()); + } + } + } + ActiveSonarrBlock::HistorySortPrompt => { + self + .app + .data + .sonarr_data + .history + .items + .sort_by(|a, b| a.id.cmp(&b.id)); + self.app.data.sonarr_data.history.apply_sorting(); + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::History => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::HistoryItemDetails.into()); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::FilterHistory | ActiveSonarrBlock::FilterHistoryError => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.history.reset_filter(); + self.app.should_ignore_quit_key = false; + } + ActiveSonarrBlock::SearchHistory | ActiveSonarrBlock::SearchHistoryError => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.history.reset_search(); + self.app.should_ignore_quit_key = false; + } + ActiveSonarrBlock::HistoryItemDetails | ActiveSonarrBlock::HistorySortPrompt => { + self.app.pop_navigation_stack(); + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::History => match self.key { + _ if key == DEFAULT_KEYBINDINGS.search.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SearchHistory.into()); + self.app.data.sonarr_data.history.search = Some(HorizontallyScrollableText::default()); + self.app.should_ignore_quit_key = true; + } + _ if key == DEFAULT_KEYBINDINGS.filter.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::FilterHistory.into()); + self.app.data.sonarr_data.history.reset_filter(); + self.app.data.sonarr_data.history.filter = Some(HorizontallyScrollableText::default()); + self.app.should_ignore_quit_key = true; + } + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ if key == DEFAULT_KEYBINDINGS.sort.key => { + self + .app + .data + .sonarr_data + .history + .sorting(history_sorting_options()); + self + .app + .push_navigation_stack(ActiveSonarrBlock::HistorySortPrompt.into()); + } + _ => (), + }, + ActiveSonarrBlock::SearchHistory => { + handle_text_box_keys!( + self, + key, + self.app.data.sonarr_data.history.search.as_mut().unwrap() + ) + } + ActiveSonarrBlock::FilterHistory => { + handle_text_box_keys!( + self, + key, + self.app.data.sonarr_data.history.filter.as_mut().unwrap() + ) + } + _ => (), + } + } +} + +fn history_sorting_options() -> Vec> { + vec![ + SortOption { + name: "Source Title", + cmp_fn: Some(|a, b| { + a.source_title + .text + .to_lowercase() + .cmp(&b.source_title.text.to_lowercase()) + }), + }, + SortOption { + name: "Event Type", + cmp_fn: Some(|a, b| { + a.event_type + .to_lowercase() + .cmp(&b.event_type.to_lowercase()) + }), + }, + SortOption { + name: "Language", + cmp_fn: Some(|a, b| { + a.language + .name + .to_lowercase() + .cmp(&b.language.name.to_lowercase()) + }), + }, + SortOption { + name: "Quality", + cmp_fn: Some(|a, b| { + a.quality + .quality + .name + .to_lowercase() + .cmp(&b.quality.quality.name.to_lowercase()) + }), + }, + SortOption { + name: "Date", + cmp_fn: Some(|a, b| a.date.cmp(&b.date)), + }, + ] +} diff --git a/src/handlers/sonarr_handlers/mod.rs b/src/handlers/sonarr_handlers/mod.rs index 18f36fd..83d277b 100644 --- a/src/handlers/sonarr_handlers/mod.rs +++ b/src/handlers/sonarr_handlers/mod.rs @@ -1,5 +1,6 @@ use blocklist::BlocklistHandler; use downloads::DownloadsHandler; +use history::HistoryHandler; use library::LibraryHandler; use crate::{ @@ -12,6 +13,7 @@ use super::KeyEventHandler; mod blocklist; mod downloads; +mod history; mod library; #[cfg(test)] @@ -41,6 +43,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SonarrHandler<'a, 'b _ if BlocklistHandler::accepts(self.active_sonarr_block) => { BlocklistHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle() } + _ if HistoryHandler::accepts(self.active_sonarr_block) => { + HistoryHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle() + } _ => self.handle_key_event(), } } diff --git a/src/handlers/sonarr_handlers/sonarr_handler_tests.rs b/src/handlers/sonarr_handlers/sonarr_handler_tests.rs index 99d438c..95de5ce 100644 --- a/src/handlers/sonarr_handlers/sonarr_handler_tests.rs +++ b/src/handlers/sonarr_handlers/sonarr_handler_tests.rs @@ -145,4 +145,24 @@ mod tests { active_sonarr_block ); } + + #[rstest] + fn test_delegates_history_blocks_to_history_handler( + #[values( + ActiveSonarrBlock::History, + ActiveSonarrBlock::HistoryItemDetails, + ActiveSonarrBlock::HistorySortPrompt, + ActiveSonarrBlock::FilterHistory, + ActiveSonarrBlock::FilterHistoryError, + ActiveSonarrBlock::SearchHistory, + ActiveSonarrBlock::SearchHistoryError + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + SonarrHandler, + ActiveSonarrBlock::History, + active_sonarr_block + ); + } } diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index 1d277c5..d7f7880 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -238,7 +238,7 @@ pub enum ActiveSonarrBlock { FilterSeriesHistory, FilterSeriesHistoryError, History, - HistoryDetails, + HistoryItemDetails, HistorySortPrompt, Indexers, IndexerSettingsConfirmPrompt, @@ -252,8 +252,6 @@ pub enum ActiveSonarrBlock { ManualSeasonSearch, ManualSeasonSearchConfirmPrompt, ManualSeasonSearchSortPrompt, - MarkHistoryItemAsFailedConfirmPrompt, - MarkHistoryItemAsFailedPrompt, RootFolders, SearchEpisodes, SearchEpisodesError, @@ -372,6 +370,16 @@ pub const DELETE_SERIES_SELECTION_BLOCKS: &[&[ActiveSonarrBlock]] = &[ &[ActiveSonarrBlock::DeleteSeriesConfirmPrompt], ]; +pub static HISTORY_BLOCKS: [ActiveSonarrBlock; 7] = [ + ActiveSonarrBlock::History, + ActiveSonarrBlock::HistoryItemDetails, + ActiveSonarrBlock::HistorySortPrompt, + ActiveSonarrBlock::FilterHistory, + ActiveSonarrBlock::FilterHistoryError, + ActiveSonarrBlock::SearchHistory, + ActiveSonarrBlock::SearchHistoryError, +]; + impl From for Route { fn from(active_sonarr_block: ActiveSonarrBlock) -> Route { Route::Sonarr(active_sonarr_block, None) diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs index e851c9b..57ec225 100644 --- a/src/models/servarr_data/sonarr/sonarr_data_tests.rs +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -204,7 +204,7 @@ mod tests { use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, ADD_SERIES_BLOCKS, ADD_SERIES_SELECTION_BLOCKS, BLOCKLIST_BLOCKS, DELETE_SERIES_BLOCKS, DELETE_SERIES_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_SERIES_BLOCKS, - EDIT_SERIES_SELECTION_BLOCKS, LIBRARY_BLOCKS, + EDIT_SERIES_SELECTION_BLOCKS, HISTORY_BLOCKS, LIBRARY_BLOCKS, }; #[test] @@ -374,5 +374,17 @@ mod tests { ); assert_eq!(delete_series_block_iter.next(), None); } + + #[test] + fn test_history_blocks_contents() { + assert_eq!(HISTORY_BLOCKS.len(), 7); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::History)); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::HistoryItemDetails)); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::HistorySortPrompt)); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::FilterHistory)); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::FilterHistoryError)); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::SearchHistory)); + assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::SearchHistoryError)); + } } } diff --git a/src/ui/sonarr_ui/blocklist/mod.rs b/src/ui/sonarr_ui/blocklist/mod.rs index 2cf068e..08448c8 100644 --- a/src/ui/sonarr_ui/blocklist/mod.rs +++ b/src/ui/sonarr_ui/blocklist/mod.rs @@ -3,7 +3,7 @@ use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, BLOCKL use crate::models::sonarr_models::BlocklistItem; use crate::models::Route; use crate::ui::styles::ManagarrStyle; -use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::utils::layout_block_top_border; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::message::Message; @@ -78,11 +78,6 @@ impl DrawUi for BlocklistUi { fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { - let current_selection = if app.data.sonarr_data.blocklist.items.is_empty() { - BlocklistItem::default() - } else { - app.data.sonarr_data.blocklist.current_selection().clone() - }; let blocklist_table_footer = app .data .sonarr_data