feat(handler): Full handler support for the Season details UI in Sonarr

This commit is contained in:
2024-12-11 23:18:37 -07:00
parent ed2211586e
commit a84324d3bc
21 changed files with 1727 additions and 115 deletions
+17 -6
View File
@@ -55,9 +55,14 @@ impl<'a> App<'a> {
.await;
}
ActiveSonarrBlock::ManualSeasonSearch => {
self
.dispatch_network_event(SonarrEvent::GetSeasonReleases(None).into())
.await;
match self.data.sonarr_data.season_details_modal.as_ref() {
Some(season_details_modal) if season_details_modal.season_releases.is_empty() => {
self
.dispatch_network_event(SonarrEvent::GetSeasonReleases(None).into())
.await;
}
_ => (),
}
}
ActiveSonarrBlock::EpisodeDetails | ActiveSonarrBlock::EpisodeFile => {
self
@@ -70,9 +75,15 @@ impl<'a> App<'a> {
.await;
}
ActiveSonarrBlock::ManualEpisodeSearch => {
self
.dispatch_network_event(SonarrEvent::GetEpisodeReleases(None).into())
.await;
if let Some(season_details_modal) = self.data.sonarr_data.season_details_modal.as_ref() {
if let Some(episode_details_modal) = season_details_modal.episode_details_modal.as_ref() {
if episode_details_modal.episode_releases.is_empty() {
self
.dispatch_network_event(SonarrEvent::GetEpisodeReleases(None).into())
.await;
}
}
}
}
ActiveSonarrBlock::Downloads => {
self
+79 -2
View File
@@ -7,8 +7,11 @@ mod tests {
use crate::{
app::App,
models::{
servarr_data::sonarr::sonarr_data::ActiveSonarrBlock,
sonarr_models::{Season, Series},
servarr_data::sonarr::{
modals::{EpisodeDetailsModal, SeasonDetailsModal},
sonarr_data::ActiveSonarrBlock,
},
sonarr_models::{Season, Series, SonarrRelease},
},
network::{sonarr_network::SonarrEvent, NetworkEvent},
};
@@ -115,6 +118,7 @@ mod tests {
#[tokio::test]
async fn test_dispatch_by_manual_season_search_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualSeasonSearch)
@@ -129,6 +133,40 @@ mod tests {
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_season_search_block_is_loading() {
let mut app = App {
is_loading: true,
..App::default()
};
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualSeasonSearch)
.await;
assert!(app.is_loading);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_season_search_block_season_releases_non_empty() {
let mut app = App::default();
let mut season_details_modal = SeasonDetailsModal::default();
season_details_modal
.season_releases
.set_items(vec![SonarrRelease::default()]);
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualSeasonSearch)
.await;
assert!(!app.is_loading);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_episode_details_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
@@ -183,6 +221,9 @@ mod tests {
#[tokio::test]
async fn test_dispatch_by_manual_episode_search_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
let mut season_details_modal = SeasonDetailsModal::default();
season_details_modal.episode_details_modal = Some(EpisodeDetailsModal::default());
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualEpisodeSearch)
@@ -197,6 +238,42 @@ mod tests {
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_episode_search_block_is_loading() {
let mut app = App {
is_loading: true,
..App::default()
};
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualEpisodeSearch)
.await;
assert!(app.is_loading);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_manual_episode_search_block_episode_releases_non_empty() {
let mut app = App::default();
let mut episode_details_modal = EpisodeDetailsModal::default();
episode_details_modal
.episode_releases
.set_items(vec![SonarrRelease::default()]);
let mut season_details_modal = SeasonDetailsModal::default();
season_details_modal.episode_details_modal = Some(episode_details_modal);
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
app
.dispatch_by_sonarr_block(&ActiveSonarrBlock::ManualEpisodeSearch)
.await;
assert!(!app.is_loading);
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_history_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
+4
View File
@@ -351,6 +351,10 @@ mod test_utils {
movie_details_modal.movie_crew.set_items(vec![$crate::models::radarr_models::Credit::default()]);
movie_details_modal.movie_releases.set_items(vec![$crate::models::radarr_models::RadarrRelease::default()]);
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
let mut season_details_modal = $crate::models::servarr_data::sonarr::modals::SeasonDetailsModal::default();
season_details_modal.season_history.set_items(vec![$crate::models::sonarr_models::SonarrHistoryItem::default()]);
season_details_modal.episode_details_modal = Some($crate::models::servarr_data::sonarr::modals::EpisodeDetailsModal::default());
app.data.sonarr_data.season_details_modal = Some(season_details_modal);
let mut series_history = $crate::models::stateful_table::StatefulTable::default();
series_history.set_items(vec![
$crate::models::sonarr_models::SonarrHistoryItem::default(),
@@ -328,31 +328,26 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler<
}
_ => (),
},
ActiveRadarrBlock::AutomaticallySearchMoviePrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::TriggerAutomaticSearch(None));
ActiveRadarrBlock::AutomaticallySearchMoviePrompt
if key == DEFAULT_KEYBINDINGS.confirm.key =>
{
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::TriggerAutomaticSearch(None));
self.app.pop_navigation_stack();
}
self.app.pop_navigation_stack();
}
ActiveRadarrBlock::UpdateAndScanPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAndScan(None));
ActiveRadarrBlock::UpdateAndScanPrompt if key == DEFAULT_KEYBINDINGS.confirm.key => {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::UpdateAndScan(None));
self.app.pop_navigation_stack();
}
self.app.pop_navigation_stack();
}
ActiveRadarrBlock::ManualSearchConfirmPrompt => {
if key == DEFAULT_KEYBINDINGS.confirm.key {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action =
Some(RadarrEvent::DownloadRelease(None));
ActiveRadarrBlock::ManualSearchConfirmPrompt if key == DEFAULT_KEYBINDINGS.confirm.key => {
self.app.data.radarr_data.prompt_confirm = true;
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DownloadRelease(None));
self.app.pop_navigation_stack();
}
self.app.pop_navigation_stack();
}
_ => (),
}
@@ -10,10 +10,7 @@ mod tests {
use crate::event::Key;
use crate::handlers::sonarr_handlers::library::{series_sorting_options, LibraryHandler};
use crate::handlers::KeyEventHandler;
use crate::models::servarr_data::sonarr::sonarr_data::{
ActiveSonarrBlock, ADD_SERIES_BLOCKS, DELETE_SERIES_BLOCKS, EDIT_SERIES_BLOCKS, LIBRARY_BLOCKS,
SERIES_DETAILS_BLOCKS,
};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS, DELETE_SERIES_BLOCKS, EDIT_SERIES_BLOCKS, LIBRARY_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS};
use crate::models::sonarr_models::{Series, SeriesStatus, SeriesType};
use crate::test_handler_delegation;
@@ -543,6 +540,33 @@ mod tests {
);
}
#[rstest]
fn test_delegates_season_details_blocks_to_season_details_handler(
#[values(
ActiveSonarrBlock::SeasonDetails,
ActiveSonarrBlock::SeasonHistory,
ActiveSonarrBlock::SearchEpisodes,
ActiveSonarrBlock::SearchEpisodesError,
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
ActiveSonarrBlock::SearchSeasonHistory,
ActiveSonarrBlock::SearchSeasonHistoryError,
ActiveSonarrBlock::FilterSeasonHistory,
ActiveSonarrBlock::FilterSeasonHistoryError,
ActiveSonarrBlock::SeasonHistorySortPrompt,
ActiveSonarrBlock::SeasonHistoryDetails,
ActiveSonarrBlock::ManualSeasonSearch,
ActiveSonarrBlock::ManualSeasonSearchSortPrompt,
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
)]
active_sonarr_block: ActiveSonarrBlock,
) {
test_handler_delegation!(
LibraryHandler,
ActiveSonarrBlock::Series,
active_sonarr_block
);
}
#[rstest]
fn test_delegates_edit_series_blocks_to_edit_series_handler(
#[values(
@@ -768,6 +792,7 @@ mod tests {
library_handler_blocks.extend(DELETE_SERIES_BLOCKS);
library_handler_blocks.extend(EDIT_SERIES_BLOCKS);
library_handler_blocks.extend(SERIES_DETAILS_BLOCKS);
library_handler_blocks.extend(SEASON_DETAILS_BLOCKS);
ActiveSonarrBlock::iter().for_each(|active_sonarr_block| {
if library_handler_blocks.contains(&active_sonarr_block) {
@@ -22,6 +22,7 @@ use crate::{
use super::handle_change_tab_left_right_keys;
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::sonarr_handlers::library::season_details_handler::SeasonDetailsHandler;
use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler;
use crate::handlers::table_handler::TableHandlingConfig;
@@ -32,6 +33,7 @@ mod delete_series_handler;
#[path = "library_handler_tests.rs"]
mod library_handler_tests;
mod series_details_handler;
mod season_details_handler;
pub(super) struct LibraryHandler<'a, 'b> {
key: Key,
@@ -75,6 +77,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, '
SeriesDetailsHandler::with(self.key, self.app, self.active_sonarr_block, self.context)
.handle();
}
_ if SeasonDetailsHandler::accepts(self.active_sonarr_block) => {
SeasonDetailsHandler::with(self.key, self.app, self.active_sonarr_block, self.context)
.handle();
}
_ => self.handle_key_event(),
}
}
@@ -85,6 +91,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, '
|| DeleteSeriesHandler::accepts(active_block)
|| EditSeriesHandler::accepts(active_block)
|| SeriesDetailsHandler::accepts(active_block)
|| SeasonDetailsHandler::accepts(active_block)
|| LIBRARY_BLOCKS.contains(&active_block)
}
@@ -0,0 +1,465 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handle_table_events;
use crate::handlers::sonarr_handlers::history::history_sorting_options;
use crate::handlers::table_handler::TableHandlingConfig;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SEASON_DETAILS_BLOCKS};
use crate::models::servarr_models::Language;
use crate::models::sonarr_models::{
Episode, SonarrHistoryItem, SonarrRelease, SonarrReleaseDownloadBody,
};
use crate::models::stateful_table::SortOption;
use crate::network::sonarr_network::SonarrEvent;
use serde_json::Number;
#[cfg(test)]
#[path = "season_details_handler_tests.rs"]
mod season_details_handler_tests;
pub(super) struct SeasonDetailsHandler<'a, 'b> {
key: Key,
app: &'a mut App<'b>,
active_sonarr_block: ActiveSonarrBlock,
_context: Option<ActiveSonarrBlock>,
}
impl<'a, 'b> SeasonDetailsHandler<'a, 'b> {
handle_table_events!(
self,
episodes,
self
.app
.data
.sonarr_data
.season_details_modal
.as_mut()
.expect("Season details modal is undefined")
.episodes,
Episode
);
handle_table_events!(
self,
season_history,
self
.app
.data
.sonarr_data
.season_details_modal
.as_mut()
.expect("Season details modal is undefined")
.season_history,
SonarrHistoryItem
);
handle_table_events!(
self,
season_releases,
self
.app
.data
.sonarr_data
.season_details_modal
.as_mut()
.expect("Season details modal is undefined")
.season_releases,
SonarrRelease
);
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler<'a, 'b> {
fn handle(&mut self) {
let episodes_table_handling_config =
TableHandlingConfig::new(ActiveSonarrBlock::SeasonDetails.into())
.searching_block(ActiveSonarrBlock::SearchEpisodes.into())
.search_error_block(ActiveSonarrBlock::SearchEpisodesError.into())
.search_field_fn(|episode: &Episode| &episode.title);
let season_history_table_handling_config =
TableHandlingConfig::new(ActiveSonarrBlock::SeasonHistory.into())
.sorting_block(ActiveSonarrBlock::SeasonHistorySortPrompt.into())
.sort_options(history_sorting_options())
.sort_by_fn(|a: &SonarrHistoryItem, b: &SonarrHistoryItem| a.id.cmp(&b.id))
.searching_block(ActiveSonarrBlock::SearchSeasonHistory.into())
.search_error_block(ActiveSonarrBlock::SearchSeasonHistoryError.into())
.search_field_fn(|history_item: &SonarrHistoryItem| &history_item.source_title.text)
.filtering_block(ActiveSonarrBlock::FilterSeasonHistory.into())
.filter_error_block(ActiveSonarrBlock::FilterSeasonHistoryError.into())
.filter_field_fn(|history_item: &SonarrHistoryItem| &history_item.source_title.text);
let season_releases_table_handling_config =
TableHandlingConfig::new(ActiveSonarrBlock::ManualSeasonSearch.into())
.sorting_block(ActiveSonarrBlock::ManualSeasonSearchSortPrompt.into())
.sort_options(releases_sorting_options());
if !self.handle_episodes_table_events(episodes_table_handling_config)
&& !self.handle_season_history_table_events(season_history_table_handling_config)
&& !self.handle_season_releases_table_events(season_releases_table_handling_config)
{
self.handle_key_event();
}
}
fn accepts(active_block: ActiveSonarrBlock) -> bool {
SEASON_DETAILS_BLOCKS.contains(&active_block)
}
fn with(
key: Key,
app: &'a mut App<'b>,
active_block: ActiveSonarrBlock,
context: Option<ActiveSonarrBlock>,
) -> SeasonDetailsHandler<'a, 'b> {
SeasonDetailsHandler {
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
&& if let Some(season_details_modal) = &self.app.data.sonarr_data.season_details_modal {
match self.active_sonarr_block {
ActiveSonarrBlock::SeasonDetails => !season_details_modal.episodes.is_empty(),
ActiveSonarrBlock::SeasonHistory => !season_details_modal.season_history.is_empty(),
ActiveSonarrBlock::ManualSeasonSearch => !season_details_modal.season_releases.is_empty(),
_ => true,
}
} else {
false
}
}
fn handle_scroll_up(&mut self) {}
fn handle_scroll_down(&mut self) {}
fn handle_home(&mut self) {}
fn handle_end(&mut self) {}
fn handle_delete(&mut self) {
if self.active_sonarr_block == ActiveSonarrBlock::SeasonDetails {
self
.app
.push_navigation_stack(ActiveSonarrBlock::DeleteEpisodeFilePrompt.into());
}
}
fn handle_left_right_action(&mut self) {
match self.active_sonarr_block {
ActiveSonarrBlock::SeasonDetails
| ActiveSonarrBlock::SeasonHistory
| ActiveSonarrBlock::ManualSeasonSearch => match self.key {
_ if self.key == DEFAULT_KEYBINDINGS.left.key => {
self
.app
.data
.sonarr_data
.season_details_modal
.as_mut()
.unwrap()
.season_details_tabs
.previous();
self.app.pop_and_push_navigation_stack(
self
.app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_details_tabs
.get_active_route(),
);
}
_ if self.key == DEFAULT_KEYBINDINGS.right.key => {
self
.app
.data
.sonarr_data
.season_details_modal
.as_mut()
.unwrap()
.season_details_tabs
.next();
self.app.pop_and_push_navigation_stack(
self
.app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_details_tabs
.get_active_route(),
);
}
_ => (),
},
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt
| ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt
| ActiveSonarrBlock::DeleteEpisodeFilePrompt => {
handle_prompt_toggle(self.app, self.key);
}
_ => (),
}
}
fn handle_submit(&mut self) {
match self.active_sonarr_block {
ActiveSonarrBlock::SeasonHistory => self
.app
.push_navigation_stack(ActiveSonarrBlock::SeasonHistoryDetails.into()),
ActiveSonarrBlock::DeleteEpisodeFilePrompt => {
if self.app.data.sonarr_data.prompt_confirm {
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::DeleteEpisodeFile(None));
}
self.app.pop_navigation_stack();
}
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt => {
if self.app.data.sonarr_data.prompt_confirm {
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::TriggerAutomaticSeasonSearch(None));
}
self.app.pop_navigation_stack();
}
ActiveSonarrBlock::ManualSeasonSearch => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt.into());
}
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt => {
if self.app.data.sonarr_data.prompt_confirm {
let SonarrRelease {
guid, indexer_id, ..
} = self
.app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_releases
.current_selection();
let series_id = self.app.data.sonarr_data.series.current_selection().id;
let season_number = self
.app
.data
.sonarr_data
.seasons
.current_selection()
.season_number;
let params = SonarrReleaseDownloadBody {
guid: guid.clone(),
indexer_id: *indexer_id,
series_id: Some(series_id),
season_number: Some(season_number),
..SonarrReleaseDownloadBody::default()
};
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::DownloadRelease(params));
}
self.app.pop_navigation_stack();
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_sonarr_block {
ActiveSonarrBlock::SeasonDetails | ActiveSonarrBlock::ManualSeasonSearch => {
self.app.pop_navigation_stack();
self.app.data.sonarr_data.season_details_modal = None;
}
ActiveSonarrBlock::SeasonHistoryDetails => {
self.app.pop_navigation_stack();
}
ActiveSonarrBlock::SeasonHistory => {
if self
.app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_history
.filtered_items
.is_some()
{
self
.app
.data
.sonarr_data
.season_details_modal
.as_mut()
.unwrap()
.season_history
.filtered_items = None;
} else {
self.app.pop_navigation_stack();
self.app.data.sonarr_data.season_details_modal = None;
}
}
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt
| ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt
| ActiveSonarrBlock::DeleteEpisodeFilePrompt => {
self.app.pop_navigation_stack();
self.app.data.sonarr_data.prompt_confirm = false;
}
_ => (),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
match self.active_sonarr_block {
ActiveSonarrBlock::SeasonDetails
| ActiveSonarrBlock::SeasonHistory
| ActiveSonarrBlock::ManualSeasonSearch => match self.key {
_ if self.key == DEFAULT_KEYBINDINGS.refresh.key => {
self
.app
.pop_and_push_navigation_stack(self.active_sonarr_block.into());
}
_ if self.key == DEFAULT_KEYBINDINGS.auto_search.key => {
self
.app
.push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchSeasonPrompt.into());
}
_ => (),
},
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt
if key == DEFAULT_KEYBINDINGS.confirm.key =>
{
self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::TriggerAutomaticSeasonSearch(None));
self.app.pop_navigation_stack();
}
ActiveSonarrBlock::DeleteEpisodeFilePrompt if key == DEFAULT_KEYBINDINGS.confirm.key => {
self.app.data.sonarr_data.prompt_confirm = true;
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::DeleteEpisodeFile(None));
self.app.pop_navigation_stack();
}
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt
if key == DEFAULT_KEYBINDINGS.confirm.key =>
{
self.app.data.sonarr_data.prompt_confirm = true;
let SonarrRelease {
guid, indexer_id, ..
} = self
.app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_releases
.current_selection();
let series_id = self.app.data.sonarr_data.series.current_selection().id;
let season_number = self
.app
.data
.sonarr_data
.seasons
.current_selection()
.season_number;
let params = SonarrReleaseDownloadBody {
guid: guid.clone(),
indexer_id: *indexer_id,
series_id: Some(series_id),
season_number: Some(season_number),
..SonarrReleaseDownloadBody::default()
};
self.app.data.sonarr_data.prompt_confirm_action =
Some(SonarrEvent::DownloadRelease(params));
self.app.pop_navigation_stack();
}
_ => (),
}
}
}
fn releases_sorting_options() -> Vec<SortOption<SonarrRelease>> {
vec![
SortOption {
name: "Source",
cmp_fn: Some(|a, b| a.protocol.cmp(&b.protocol)),
},
SortOption {
name: "Age",
cmp_fn: Some(|a, b| a.age.cmp(&b.age)),
},
SortOption {
name: "Rejected",
cmp_fn: Some(|a, b| a.rejected.cmp(&b.rejected)),
},
SortOption {
name: "Title",
cmp_fn: Some(|a, b| {
a.title
.text
.to_lowercase()
.cmp(&b.title.text.to_lowercase())
}),
},
SortOption {
name: "Indexer",
cmp_fn: Some(|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase())),
},
SortOption {
name: "Size",
cmp_fn: Some(|a, b| a.size.cmp(&b.size)),
},
SortOption {
name: "Peers",
cmp_fn: Some(|a, b| {
let default_number = Number::from(i64::MAX);
let seeder_a = a
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
let seeder_b = b
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
seeder_a.cmp(&seeder_b)
}),
},
SortOption {
name: "Language",
cmp_fn: Some(|a, b| {
let default_language_vec = vec![Language {
id: 1,
name: "_".to_owned(),
}];
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
let language_b = &b.languages.as_ref().unwrap_or(&default_language_vec)[0];
language_a.cmp(language_b)
}),
},
SortOption {
name: "Quality",
cmp_fn: Some(|a, b| a.quality.cmp(&b.quality)),
},
]
}
@@ -0,0 +1,982 @@
#[cfg(test)]
mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::handlers::sonarr_handlers::library::season_details_handler::{
releases_sorting_options, SeasonDetailsHandler,
};
use crate::handlers::KeyEventHandler;
use crate::models::servarr_data::sonarr::modals::SeasonDetailsModal;
use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data;
use crate::models::servarr_data::sonarr::sonarr_data::{
ActiveSonarrBlock, SEASON_DETAILS_BLOCKS,
};
use crate::models::servarr_models::{Language, Quality, QualityWrapper};
use crate::models::sonarr_models::{SonarrRelease, SonarrReleaseDownloadBody};
use crate::models::HorizontallyScrollableText;
use pretty_assertions::assert_str_eq;
use rstest::rstest;
use serde_json::Number;
use std::cmp::Ordering;
use strum::IntoEnumIterator;
mod test_handle_delete {
use super::*;
use crate::event::Key;
use pretty_assertions::assert_eq;
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
#[test]
fn test_delete_episode_prompt() {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
SeasonDetailsHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::SeasonDetails, None)
.handle();
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::DeleteEpisodeFilePrompt.into()
);
}
#[test]
fn test_delete_episode_prompt_no_op_when_not_ready() {
let mut app = App::default();
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
app.is_loading = true;
SeasonDetailsHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::SeasonDetails, None)
.handle();
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::SeasonDetails.into()
);
}
}
mod test_handle_left_right_actions {
use super::*;
use crate::event::Key;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
fn test_left_right_prompt_toggle(
#[values(
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt,
ActiveSonarrBlock::DeleteEpisodeFilePrompt
)]
active_sonarr_block: ActiveSonarrBlock,
#[values(Key::Left, Key::Right)] key: Key,
) {
let mut app = App::default();
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
SeasonDetailsHandler::with(key, &mut app, active_sonarr_block, None).handle();
assert!(app.data.sonarr_data.prompt_confirm);
SeasonDetailsHandler::with(key, &mut app, active_sonarr_block, None).handle();
assert!(!app.data.sonarr_data.prompt_confirm);
}
#[rstest]
#[case(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory)]
#[case(
ActiveSonarrBlock::SeasonHistory,
ActiveSonarrBlock::ManualSeasonSearch
)]
#[case(
ActiveSonarrBlock::ManualSeasonSearch,
ActiveSonarrBlock::SeasonDetails
)]
fn test_season_details_tabs_left_right_action(
#[case] left_block: ActiveSonarrBlock,
#[case] right_block: ActiveSonarrBlock,
#[values(true, false)] is_ready: bool,
) {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into());
app.is_loading = is_ready;
app.push_navigation_stack(right_block.into());
app
.data
.sonarr_data
.season_details_modal
.as_mut()
.unwrap()
.season_details_tabs
.index = app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_details_tabs
.tabs
.iter()
.position(|tab_route| tab_route.route == right_block.into())
.unwrap_or_default();
SeasonDetailsHandler::with(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None)
.handle();
assert_eq!(
app.get_current_route(),
app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_details_tabs
.get_active_route()
);
assert_eq!(app.get_current_route(), left_block.into());
SeasonDetailsHandler::with(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None)
.handle();
assert_eq!(
app.get_current_route(),
app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_details_tabs
.get_active_route()
);
assert_eq!(app.get_current_route(), right_block.into());
}
}
mod test_handle_submit {
use super::*;
use crate::event::Key;
use crate::models::stateful_table::StatefulTable;
use crate::network::sonarr_network::SonarrEvent;
use pretty_assertions::assert_eq;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[test]
fn test_season_history_submit() {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeasonHistory, None)
.handle();
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::SeasonHistoryDetails.into()
);
}
#[test]
fn test_season_history_submit_no_op_when_season_history_is_empty() {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app
.data
.sonarr_data
.season_details_modal
.as_mut()
.unwrap()
.season_history = StatefulTable::default();
app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into());
SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeasonHistory, None)
.handle();
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::SeasonHistory.into()
);
}
#[test]
fn test_season_history_submit_no_op_when_not_ready() {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.is_loading = true;
app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into());
SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeasonHistory, None)
.handle();
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::SeasonHistory.into()
);
}
#[rstest]
#[case(
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
SonarrEvent::TriggerAutomaticSeasonSearch(None)
)]
#[case(
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
SonarrEvent::DeleteEpisodeFile(None)
)]
fn test_season_details_prompt_confirm_submit(
#[case] prompt_block: ActiveSonarrBlock,
#[case] expected_action: SonarrEvent,
#[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.data.sonarr_data.prompt_confirm = true;
app.push_navigation_stack(active_sonarr_block.into());
app.push_navigation_stack(prompt_block.into());
SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(app.data.sonarr_data.prompt_confirm);
assert_eq!(app.get_current_route(), active_sonarr_block.into());
assert_eq!(
app.data.sonarr_data.prompt_confirm_action,
Some(expected_action)
);
}
#[test]
fn test_season_details_manual_search_confirm_prompt_confirm_submit() {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.data.sonarr_data.prompt_confirm = true;
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into());
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt.into());
SeasonDetailsHandler::with(
SUBMIT_KEY,
&mut app,
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt,
None,
)
.handle();
assert!(app.data.sonarr_data.prompt_confirm);
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::ManualSeasonSearch.into()
);
assert_eq!(
app.data.sonarr_data.prompt_confirm_action,
Some(SonarrEvent::DownloadRelease(SonarrReleaseDownloadBody {
guid: String::new(),
indexer_id: 0,
series_id: Some(0),
season_number: Some(0),
..SonarrReleaseDownloadBody::default()
}))
);
}
#[rstest]
fn test_season_details_prompt_decline_submit(
#[values(
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt
)]
prompt_block: ActiveSonarrBlock,
) {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
app.push_navigation_stack(prompt_block.into());
SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::SeasonDetails.into()
);
assert_eq!(app.data.sonarr_data.prompt_confirm_action, None);
}
#[test]
fn test_manual_season_search_submit() {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into());
SeasonDetailsHandler::with(
SUBMIT_KEY,
&mut app,
ActiveSonarrBlock::ManualSeasonSearch,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt.into()
);
}
#[test]
fn test_manual_season_search_submit_no_op_when_not_ready() {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.is_loading = true;
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into());
SeasonDetailsHandler::with(
SUBMIT_KEY,
&mut app,
ActiveSonarrBlock::ManualSeasonSearch,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::ManualSeasonSearch.into()
);
}
}
mod test_handle_esc {
use super::*;
use crate::event::Key;
use crate::models::sonarr_models::SonarrHistoryItem;
use crate::models::stateful_table::StatefulTable;
use pretty_assertions::assert_eq;
use ratatui::widgets::TableState;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[test]
fn test_season_history_details_block_esc() {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into());
app.push_navigation_stack(ActiveSonarrBlock::SeasonHistoryDetails.into());
SeasonDetailsHandler::with(
ESC_KEY,
&mut app,
ActiveSonarrBlock::SeasonHistoryDetails,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::SeasonHistory.into()
);
}
#[rstest]
fn test_season_details_prompts_esc(
#[values(
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt
)]
prompt_block: ActiveSonarrBlock,
#[values(true, false)] is_ready: bool,
) {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.is_loading = is_ready;
app.data.sonarr_data.prompt_confirm = true;
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
app.push_navigation_stack(prompt_block.into());
SeasonDetailsHandler::with(ESC_KEY, &mut app, prompt_block, None).handle();
assert!(!app.data.sonarr_data.prompt_confirm);
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::SeasonDetails.into()
);
}
#[test]
fn test_season_history_esc_resets_filter_if_one_is_set_instead_of_closing_the_window() {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
let mut season_history = StatefulTable {
filter: Some("Test".into()),
filtered_items: Some(vec![SonarrHistoryItem::default()]),
filtered_state: Some(TableState::default()),
..StatefulTable::default()
};
season_history.set_items(vec![SonarrHistoryItem::default()]);
app.data.sonarr_data.season_details_modal.as_mut().unwrap().season_history = season_history;
app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into());
app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into());
SeasonDetailsHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::SeasonHistory, None)
.handle();
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::SeasonHistory.into()
);
assert!(app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_history
.filter
.is_none());
assert!(app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_history
.filtered_items
.is_none());
assert!(app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_history
.filtered_state
.is_none());
}
#[rstest]
fn test_season_details_tabs_esc(
#[values(
ActiveSonarrBlock::SeasonDetails,
ActiveSonarrBlock::SeasonHistory,
ActiveSonarrBlock::ManualSeasonSearch
)] active_sonarr_block: ActiveSonarrBlock
) {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into());
app.push_navigation_stack(active_sonarr_block.into());
SeasonDetailsHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle();
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::SeriesDetails.into()
);
assert!(app.data.sonarr_data.season_details_modal.is_none());
}
}
mod test_handle_key_char {
use super::*;
use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data;
use crate::network::sonarr_network::SonarrEvent;
use pretty_assertions::assert_eq;
#[rstest]
fn test_auto_search_key(
#[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory, ActiveSonarrBlock::ManualSeasonSearch)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.push_navigation_stack(active_sonarr_block.into());
SeasonDetailsHandler::with(
DEFAULT_KEYBINDINGS.auto_search.key,
&mut app,
active_sonarr_block,
None,
)
.handle();
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt.into()
);
}
#[rstest]
fn test_auto_search_key_no_op_when_not_ready(
#[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory, ActiveSonarrBlock::ManualSeasonSearch)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::default();
app.is_loading = true;
app.push_navigation_stack(active_sonarr_block.into());
SeasonDetailsHandler::with(
DEFAULT_KEYBINDINGS.auto_search.key,
&mut app,
active_sonarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), active_sonarr_block.into());
}
#[rstest]
fn test_refresh_key(
#[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory, ActiveSonarrBlock::ManualSeasonSearch)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.push_navigation_stack(active_sonarr_block.into());
app.is_routing = false;
SeasonDetailsHandler::with(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
active_sonarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), active_sonarr_block.into());
assert!(app.is_routing);
}
#[rstest]
fn test_refresh_key_no_op_when_not_ready(
#[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory, ActiveSonarrBlock::ManualSeasonSearch)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.is_loading = true;
app.push_navigation_stack(active_sonarr_block.into());
app.is_routing = false;
SeasonDetailsHandler::with(
DEFAULT_KEYBINDINGS.refresh.key,
&mut app,
active_sonarr_block,
None,
)
.handle();
assert_eq!(app.get_current_route(), active_sonarr_block.into());
assert!(!app.is_routing);
}
#[rstest]
#[case(
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
SonarrEvent::TriggerAutomaticSeasonSearch(None)
)]
#[case(
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
SonarrEvent::DeleteEpisodeFile(None)
)]
fn test_season_details_prompt_confirm_confirm_key(
#[case] prompt_block: ActiveSonarrBlock,
#[case] expected_action: SonarrEvent,
#[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.data.sonarr_data.prompt_confirm = true;
app.push_navigation_stack(active_sonarr_block.into());
app.push_navigation_stack(prompt_block.into());
SeasonDetailsHandler::with(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
prompt_block,
None,
)
.handle();
assert!(app.data.sonarr_data.prompt_confirm);
assert_eq!(app.get_current_route(), active_sonarr_block.into());
assert_eq!(
app.data.sonarr_data.prompt_confirm_action,
Some(expected_action)
);
}
#[test]
fn test_season_details_manual_search_confirm_prompt_confirm_confirm_key() {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.data.sonarr_data.prompt_confirm = true;
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into());
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt.into());
SeasonDetailsHandler::with(
DEFAULT_KEYBINDINGS.confirm.key,
&mut app,
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt,
None,
)
.handle();
assert!(app.data.sonarr_data.prompt_confirm);
assert_eq!(
app.get_current_route(),
ActiveSonarrBlock::ManualSeasonSearch.into()
);
assert_eq!(
app.data.sonarr_data.prompt_confirm_action,
Some(SonarrEvent::DownloadRelease(SonarrReleaseDownloadBody {
guid: String::new(),
indexer_id: 0,
series_id: Some(0),
season_number: Some(0),
..SonarrReleaseDownloadBody::default()
}))
);
}
}
#[test]
fn test_season_details_handler_accepts() {
ActiveSonarrBlock::iter().for_each(|active_sonarr_block| {
if SEASON_DETAILS_BLOCKS.contains(&active_sonarr_block) {
assert!(SeasonDetailsHandler::accepts(active_sonarr_block));
} else {
assert!(!SeasonDetailsHandler::accepts(active_sonarr_block));
}
});
}
#[test]
fn test_season_details_handler_is_not_ready_when_loading() {
let mut app = App::default();
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
app.is_loading = true;
let handler = SeasonDetailsHandler::with(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::SeasonDetails,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_season_details_handler_is_not_ready_when_not_loading_and_season_details_is_none() {
let mut app = App::default();
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
let handler = SeasonDetailsHandler::with(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::SeasonDetails,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_season_details_handler_is_not_ready_when_not_loading_and_episodes_table_is_empty() {
let mut app = App::default();
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
let handler = SeasonDetailsHandler::with(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::SeasonDetails,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_season_details_handler_is_not_ready_when_not_loading_and_history_table_is_empty() {
let mut app = App::default();
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into());
let handler = SeasonDetailsHandler::with(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::SeasonHistory,
None,
);
assert!(!handler.is_ready());
}
#[test]
fn test_season_details_handler_is_not_ready_when_not_loading_and_releases_table_is_empty() {
let mut app = App::default();
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into());
let handler = SeasonDetailsHandler::with(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
ActiveSonarrBlock::ManualSeasonSearch,
None,
);
assert!(!handler.is_ready());
}
#[rstest]
fn test_season_details_handler_is_ready_when_not_loading_and_season_details_modal_is_populated(
#[values(
ActiveSonarrBlock::SeasonDetails,
ActiveSonarrBlock::SeasonHistory,
ActiveSonarrBlock::ManualSeasonSearch
)]
active_sonarr_block: ActiveSonarrBlock,
) {
let mut app = App::default();
app.data.sonarr_data = create_test_sonarr_data();
app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into());
app.push_navigation_stack(active_sonarr_block.into());
let handler = SeasonDetailsHandler::with(
DEFAULT_KEYBINDINGS.esc.key,
&mut app,
active_sonarr_block,
None,
);
assert!(handler.is_ready());
}
#[test]
fn test_releases_sorting_options_source() {
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering =
|a, b| a.protocol.cmp(&b.protocol);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[0].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Source");
}
#[test]
fn test_releases_sorting_options_age() {
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| a.age.cmp(&b.age);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[1].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Age");
}
#[test]
fn test_releases_sorting_options_rejected() {
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering =
|a, b| a.rejected.cmp(&b.rejected);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[2].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Rejected");
}
#[test]
fn test_releases_sorting_options_title() {
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| {
a.title
.text
.to_lowercase()
.cmp(&b.title.text.to_lowercase())
};
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[3].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Title");
}
#[test]
fn test_releases_sorting_options_indexer() {
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering =
|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase());
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[4].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Indexer");
}
#[test]
fn test_releases_sorting_options_size() {
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering =
|a, b| a.size.cmp(&b.size);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[5].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Size");
}
#[test]
fn test_releases_sorting_options_peers() {
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| {
let default_number = Number::from(i64::MAX);
let seeder_a = a
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
let seeder_b = b
.seeders
.as_ref()
.unwrap_or(&default_number)
.as_u64()
.unwrap();
seeder_a.cmp(&seeder_b)
};
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[6].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Peers");
}
#[test]
fn test_releases_sorting_options_language() {
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| {
let default_language_vec = vec![Language {
id: 1,
name: "_".to_owned(),
}];
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
let language_b = &b.languages.as_ref().unwrap_or(&default_language_vec)[0];
language_a.cmp(language_b)
};
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[7].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Language");
}
#[test]
fn test_releases_sorting_options_quality() {
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering =
|a, b| a.quality.cmp(&b.quality);
let mut expected_releases_vec = release_vec();
expected_releases_vec.sort_by(expected_cmp_fn);
let sort_option = releases_sorting_options()[8].clone();
let mut sorted_releases_vec = release_vec();
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_releases_vec, expected_releases_vec);
assert_str_eq!(sort_option.name, "Quality");
}
fn release_vec() -> Vec<SonarrRelease> {
let release_a = SonarrRelease {
protocol: "Protocol A".to_owned(),
age: 1,
title: HorizontallyScrollableText::from("Title A"),
indexer: "Indexer A".to_owned(),
size: 1,
rejected: true,
seeders: Some(Number::from(1)),
languages: Some(vec![Language {
id: 1,
name: "Language A".to_owned(),
}]),
quality: QualityWrapper {
quality: Quality {
name: "Quality A".to_owned(),
},
},
..SonarrRelease::default()
};
let release_b = SonarrRelease {
protocol: "Protocol B".to_owned(),
age: 2,
title: HorizontallyScrollableText::from("title B"),
indexer: "indexer B".to_owned(),
size: 2,
rejected: false,
seeders: Some(Number::from(2)),
languages: Some(vec![Language {
id: 2,
name: "Language B".to_owned(),
}]),
quality: QualityWrapper {
quality: Quality {
name: "Quality B".to_owned(),
},
},
..SonarrRelease::default()
};
let release_c = SonarrRelease {
protocol: "Protocol C".to_owned(),
age: 3,
title: HorizontallyScrollableText::from("Title C"),
indexer: "Indexer C".to_owned(),
size: 3,
rejected: false,
seeders: None,
languages: None,
quality: QualityWrapper {
quality: Quality {
name: "Quality C".to_owned(),
},
},
..SonarrRelease::default()
};
vec![release_a, release_b, release_c]
}
}
+7 -7
View File
@@ -329,22 +329,22 @@ impl Default for SeasonDetailsModal {
TabRoute {
title: "Episodes",
route: ActiveSonarrBlock::SeasonDetails.into(),
help: build_context_clue_string(&SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES)),
help: build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES)),
},
TabRoute {
title: "History",
route: ActiveSonarrBlock::SeasonHistory.into(),
help: build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES)),
help: build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)),
},
TabRoute {
title: "Manual Search",
route: ActiveSonarrBlock::ManualSeasonSearch.into(),
help: build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES),
contextual_help: Some(build_context_clue_string(
help: build_context_clue_string(
&MANUAL_SEASON_SEARCH_CONTEXT_CLUES,
)),
),
contextual_help: Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)),
},
]),
}
@@ -338,11 +338,11 @@ mod tests {
);
assert_str_eq!(
season_details_modal.season_details_tabs.tabs[0].help,
build_context_clue_string(&SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES)
build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES)
);
assert_eq!(
season_details_modal.season_details_tabs.tabs[0].contextual_help,
Some(build_context_clue_string(&SEASON_DETAILS_CONTEXT_CLUES))
Some(build_context_clue_string(&SEASON_DETAILS_CONTEXTUAL_CONTEXT_CLUES))
);
assert_str_eq!(
@@ -355,11 +355,11 @@ mod tests {
);
assert_str_eq!(
season_details_modal.season_details_tabs.tabs[1].help,
build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)
build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES)
);
assert_eq!(
season_details_modal.season_details_tabs.tabs[1].contextual_help,
Some(build_context_clue_string(&SEASON_HISTORY_CONTEXT_CLUES))
Some(build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES))
);
assert_str_eq!(
@@ -372,12 +372,12 @@ mod tests {
);
assert_str_eq!(
season_details_modal.season_details_tabs.tabs[2].help,
build_context_clue_string(&DETAILS_CONTEXTUAL_CONTEXT_CLUES)
build_context_clue_string(&MANUAL_SEASON_SEARCH_CONTEXT_CLUES)
);
assert_eq!(
season_details_modal.season_details_tabs.tabs[2].contextual_help,
Some(build_context_clue_string(
&MANUAL_SEASON_SEARCH_CONTEXT_CLUES
&DETAILS_CONTEXTUAL_CONTEXT_CLUES
))
);
}
@@ -333,11 +333,11 @@ pub static SERIES_DETAILS_BLOCKS: [ActiveSonarrBlock; 12] = [
ActiveSonarrBlock::SeriesHistoryDetails,
];
pub static SEASON_DETAILS_BLOCKS: [ActiveSonarrBlock; 14] = [
pub static SEASON_DETAILS_BLOCKS: [ActiveSonarrBlock; 15] = [
ActiveSonarrBlock::SeasonDetails,
ActiveSonarrBlock::SeasonHistory,
ActiveSonarrBlock::SearchSeason,
ActiveSonarrBlock::SearchSeasonError,
ActiveSonarrBlock::SearchEpisodes,
ActiveSonarrBlock::SearchEpisodesError,
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
ActiveSonarrBlock::SearchSeasonHistory,
ActiveSonarrBlock::SearchSeasonHistoryError,
@@ -348,6 +348,7 @@ pub static SEASON_DETAILS_BLOCKS: [ActiveSonarrBlock; 14] = [
ActiveSonarrBlock::ManualSeasonSearch,
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt,
ActiveSonarrBlock::ManualSeasonSearchSortPrompt,
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
];
pub static ADD_SERIES_BLOCKS: [ActiveSonarrBlock; 13] = [
@@ -608,11 +608,11 @@ mod tests {
#[test]
fn test_season_details_blocks_contents() {
assert_eq!(SEASON_DETAILS_BLOCKS.len(), 14);
assert_eq!(SEASON_DETAILS_BLOCKS.len(), 15);
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeasonDetails));
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeasonHistory));
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeason));
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeasonError));
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchEpisodes));
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchEpisodesError));
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::AutomaticallySearchSeasonPrompt));
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeasonHistory));
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeasonHistoryError));
@@ -623,6 +623,7 @@ mod tests {
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::ManualSeasonSearch));
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt));
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::ManualSeasonSearchSortPrompt));
assert!(SEASON_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::DeleteEpisodeFilePrompt));
}
}
}
@@ -27,6 +27,7 @@ pub mod utils {
season_details_modal
.episodes
.set_items(vec![Episode::default()]);
season_details_modal.season_history.set_items(vec![SonarrHistoryItem::default()]);
season_details_modal
.season_releases
.set_items(vec![SonarrRelease::default()]);
+2 -2
View File
@@ -210,7 +210,7 @@ pub struct Episode {
pub season_number: i64,
#[serde(deserialize_with = "super::from_i64")]
pub episode_number: i64,
pub title: Option<String>,
pub title: String,
pub air_date_utc: Option<DateTime<Utc>>,
pub overview: Option<String>,
pub has_file: bool,
@@ -220,7 +220,7 @@ pub struct Episode {
impl Display for Episode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.title.as_ref().unwrap_or(&String::new()))
write!(f, "{}", self.title)
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ mod tests {
#[test]
fn test_episode_display() {
let episode = Episode {
title: Some("Test Title".to_owned()),
title: "Test Title".to_owned(),
..Episode::default()
};
+3 -3
View File
@@ -4,6 +4,8 @@ use log::{debug, info, warn};
use serde_json::{json, Value};
use urlencoding::encode;
use super::{Network, NetworkEvent, NetworkResource};
use crate::models::sonarr_models::DownloadStatus;
use crate::{
models::{
radarr_models::IndexerTestResult,
@@ -31,8 +33,6 @@ use crate::{
network::RequestMethod,
utils::convert_to_gb,
};
use crate::models::sonarr_models::DownloadStatus;
use super::{Network, NetworkEvent, NetworkResource};
#[cfg(test)]
#[path = "sonarr_network_tests.rs"]
mod sonarr_network_tests;
@@ -1568,7 +1568,7 @@ impl<'a, 'b> Network<'a, 'b> {
Air Date: {air_date}
Status: {status}
Description: {}",
title.unwrap_or_default(),
title,
overview.unwrap_or_default(),
)),
..EpisodeDetailsModal::default()
+13 -22
View File
@@ -15,7 +15,10 @@ mod test {
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use crate::models::sonarr_models::{AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, AddSeriesSearchResultStatistics, DownloadStatus, EditSeriesParams, IndexerSettings, SeriesMonitor, SonarrHistoryEventType};
use crate::models::sonarr_models::{
AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, AddSeriesSearchResultStatistics,
DownloadStatus, EditSeriesParams, IndexerSettings, SeriesMonitor, SonarrHistoryEventType,
};
use crate::app::{App, ServarrConfig};
use crate::models::radarr_models::IndexerTestResult;
@@ -2234,13 +2237,13 @@ mod test {
#[tokio::test]
async fn test_handle_get_episodes_event(#[values(true, false)] use_custom_sorting: bool) {
let episode_1 = Episode {
title: Some("z test".to_owned()),
title: "z test".to_owned(),
episode_file: None,
..episode()
};
let episode_2 = Episode {
id: 2,
title: Some("A test".to_owned()),
title: "A test".to_owned(),
episode_file_id: 2,
season_number: 2,
episode_number: 2,
@@ -2249,7 +2252,7 @@ mod test {
};
let episode_3 = Episode {
id: 3,
title: Some("A test".to_owned()),
title: "A test".to_owned(),
episode_file_id: 3,
season_number: 1,
episode_number: 2,
@@ -2271,13 +2274,7 @@ mod test {
let mut season_details_modal = SeasonDetailsModal::default();
season_details_modal.episodes.sort_asc = true;
if use_custom_sorting {
let cmp_fn = |a: &Episode, b: &Episode| {
a.title
.as_ref()
.unwrap()
.to_lowercase()
.cmp(&b.title.as_ref().unwrap().to_lowercase())
};
let cmp_fn = |a: &Episode, b: &Episode| a.title.to_lowercase().cmp(&b.title.to_lowercase());
expected_sorted_episodes.sort_by(cmp_fn);
let title_sort_option = SortOption {
name: "Title",
@@ -2348,13 +2345,13 @@ mod test {
#[tokio::test]
async fn test_handle_get_episodes_event_empty_seasons_table_returns_all_episodes_by_default() {
let episode_1 = Episode {
title: Some("z test".to_owned()),
title: "z test".to_owned(),
episode_file: None,
..episode()
};
let episode_2 = Episode {
id: 2,
title: Some("A test".to_owned()),
title: "A test".to_owned(),
episode_file_id: 2,
season_number: 2,
episode_number: 2,
@@ -2363,7 +2360,7 @@ mod test {
};
let episode_3 = Episode {
id: 3,
title: Some("A test".to_owned()),
title: "A test".to_owned(),
episode_file_id: 3,
season_number: 1,
episode_number: 2,
@@ -2537,13 +2534,7 @@ mod test {
.push_navigation_stack(ActiveSonarrBlock::EpisodesSortPrompt.into());
let mut season_details_modal = SeasonDetailsModal::default();
season_details_modal.episodes.sort_asc = true;
let cmp_fn = |a: &Episode, b: &Episode| {
a.title
.as_ref()
.unwrap()
.to_lowercase()
.cmp(&b.title.as_ref().unwrap().to_lowercase())
};
let cmp_fn = |a: &Episode, b: &Episode| a.title.to_lowercase().cmp(&b.title.to_lowercase());
expected_episodes.sort_by(cmp_fn);
let title_sort_option = SortOption {
name: "Title",
@@ -7255,7 +7246,7 @@ mod test {
episode_file_id: 1,
season_number: 1,
episode_number: 1,
title: Some("Something cool".to_owned()),
title: "Something cool".to_owned(),
air_date_utc: Some(DateTime::from(
DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap(),
)),
+84 -35
View File
@@ -1,9 +1,16 @@
use crate::app::App;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SEASON_DETAILS_BLOCKS};
use crate::models::sonarr_models::{
DownloadRecord, DownloadStatus, Episode, SonarrHistoryItem, SonarrRelease,
DownloadRecord, DownloadStatus, Episode, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease,
};
use crate::models::Route;
use crate::ui::sonarr_ui::sonarr_ui_utils::{
create_download_failed_history_event_details,
create_download_folder_imported_history_event_details,
create_episode_file_deleted_history_event_details,
create_episode_file_renamed_history_event_details, create_grabbed_history_event_details,
create_no_data_history_event_details,
};
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{
borderless_block, decorate_peer_style, get_width_from_percentage, layout_block_top_border,
@@ -11,12 +18,13 @@ use crate::ui::utils::{
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::{draw_popup, draw_tabs, DrawUi};
use crate::utils::convert_to_gb;
use chrono::Utc;
use ratatui::layout::{Constraint, Rect};
use ratatui::prelude::{Line, Stylize, Text};
use ratatui::layout::{Alignment, Constraint, Rect};
use ratatui::prelude::{Line, Style, Stylize, Text};
use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
use ratatui::Frame;
@@ -37,20 +45,12 @@ impl DrawUi for SeasonDetailsUi {
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, _area: Rect) {
if app.data.sonarr_data.season_details_modal.is_some() {
if let Route::Sonarr(active_sonarr_block, _) = app
.data
.sonarr_data
.season_details_modal
.as_ref()
.unwrap()
.season_details_tabs
.get_active_route()
{
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
let draw_season_details_popup = |f: &mut Frame<'_>, app: &mut App<'_>, popup_area: Rect| {
let content_area = draw_tabs(
f,
popup_area,
"Season Details",
&format!("Season {} Details", app.data.sonarr_data.seasons.current_selection().season_number),
&app
.data
.sonarr_data
@@ -64,8 +64,8 @@ impl DrawUi for SeasonDetailsUi {
match active_sonarr_block {
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt => {
let prompt = format!(
"Do you want to trigger an automatic search of your indexers for season packs for the season: Season {}",
app.data.sonarr_data.seasons.current_selection().season_number
"Do you want to trigger an automatic search of your indexers for season packs for: {}",
app.data.sonarr_data.seasons.current_selection().title.as_ref().unwrap()
);
let confirmation_prompt = ConfirmationPrompt::new()
.title("Automatic Season Search")
@@ -89,8 +89,6 @@ impl DrawUi for SeasonDetailsUi {
.episodes
.current_selection()
.title
.as_ref()
.unwrap_or(&String::new())
);
let confirmation_prompt = ConfirmationPrompt::new()
.title("Delete Episode")
@@ -105,11 +103,14 @@ impl DrawUi for SeasonDetailsUi {
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt => {
draw_manual_season_search_confirm_prompt(f, app);
}
ActiveSonarrBlock::SeasonHistoryDetails => {
draw_history_item_details_popup(f, app, popup_area);
}
_ => (),
}
};
draw_popup(f, app, draw_season_details_popup, Size::Large);
draw_popup(f, app, draw_season_details_popup, Size::XLarge);
}
}
}
@@ -194,20 +195,20 @@ fn draw_episodes_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Row::new(vec![
Cell::from(episode_monitored.to_owned()),
Cell::from(episode_number.to_string()),
Cell::from(title.clone().unwrap_or_default()),
Cell::from(title.clone()),
Cell::from(air_date),
Cell::from(format!("{size:.2} GB")),
Cell::from(quality_profile),
]),
)
};
let is_searching = active_sonarr_block == ActiveSonarrBlock::SearchSeason;
let is_searching = active_sonarr_block == ActiveSonarrBlock::SearchEpisodes;
let season_table = ManagarrTable::new(content, episode_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading)
.footer(help_footer)
.searching(active_sonarr_block == ActiveSonarrBlock::SearchSeason)
.search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeasonError)
.searching(is_searching)
.search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchEpisodesError)
.headers([
"🏷",
"#",
@@ -329,13 +330,14 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
match app.data.sonarr_data.season_details_modal.as_ref() {
Some(season_details_modal) if !app.is_loading => {
let current_selection = if season_details_modal.season_releases.is_empty() {
SonarrRelease::default()
let (current_selection, is_empty) = if season_details_modal.season_releases.is_empty() {
(SonarrRelease::default(), true)
} else {
season_details_modal
(season_details_modal
.season_releases
.current_selection()
.clone()
.clone(),
season_details_modal.season_releases.is_empty())
};
let season_release_table_footer = season_details_modal
.season_details_tabs
@@ -409,16 +411,22 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let release_table =
ManagarrTable::new(Some(&mut season_release_table), season_release_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading)
.loading(app.is_loading || is_empty)
.footer(season_release_table_footer)
.sorting(active_sonarr_block == ActiveSonarrBlock::ManualSeasonSearchSortPrompt)
.headers(["Source Title", "Event Type", "Language", "Quality", "Date"])
.headers([
"Source", "Age", "", "Title", "Indexer", "Size", "Peers", "Language", "Quality",
])
.constraints([
Constraint::Percentage(40),
Constraint::Percentage(15),
Constraint::Percentage(12),
Constraint::Percentage(13),
Constraint::Percentage(20),
Constraint::Length(9),
Constraint::Length(10),
Constraint::Length(5),
Constraint::Percentage(30),
Constraint::Percentage(18),
Constraint::Length(12),
Constraint::Length(12),
Constraint::Percentage(7),
Constraint::Percentage(10),
]);
f.render_widget(release_table, area);
@@ -479,14 +487,14 @@ fn draw_manual_season_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>
.title(title)
.prompt(&prompt)
.content(content_paragraph)
.yes_no_value(app.data.radarr_data.prompt_confirm);
.yes_no_value(app.data.sonarr_data.prompt_confirm);
f.render_widget(Popup::new(confirmation_prompt).size(Size::Small), f.area());
} else {
let confirmation_prompt = ConfirmationPrompt::new()
.title(title)
.prompt(&prompt)
.yes_no_value(app.data.radarr_data.prompt_confirm);
.yes_no_value(app.data.sonarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
@@ -495,6 +503,47 @@ fn draw_manual_season_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>
}
}
fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let current_selection =
if let Some(season_details_modal) = app.data.sonarr_data.season_details_modal.as_ref() {
if season_details_modal.season_history.is_empty() {
SonarrHistoryItem::default()
} else {
season_details_modal
.season_history
.current_selection()
.clone()
}
} else {
SonarrHistoryItem::default()
};
let line_vec = match current_selection.event_type {
SonarrHistoryEventType::Grabbed => create_grabbed_history_event_details(current_selection),
SonarrHistoryEventType::DownloadFolderImported => {
create_download_folder_imported_history_event_details(current_selection)
}
SonarrHistoryEventType::DownloadFailed => {
create_download_failed_history_event_details(current_selection)
}
SonarrHistoryEventType::EpisodeFileDeleted => {
create_episode_file_deleted_history_event_details(current_selection)
}
SonarrHistoryEventType::EpisodeFileRenamed => {
create_episode_file_renamed_history_event_details(current_selection)
}
_ => create_no_data_history_event_details(current_selection),
};
let text = Text::from(line_vec);
let message = Message::new(text)
.title("Details")
.style(Style::new().secondary())
.alignment(Alignment::Left);
f.render_widget(Popup::new(message).size(Size::NarrowMessage), area);
}
fn decorate_with_row_style<'a>(
downloads_vec: &[DownloadRecord],
episode: &Episode,
@@ -106,7 +106,7 @@ impl DrawUi for SeriesDetailsUi {
};
draw_popup(f, app, draw_series_details_popup, Size::XXLarge);
if SeasonDetailsUi::accepts(route) {
SeasonDetailsUi::draw(f, app, area);
}
+2
View File
@@ -22,6 +22,7 @@ pub enum Size {
Small,
Medium,
Large,
XLarge,
XXLarge,
Long,
}
@@ -41,6 +42,7 @@ impl Size {
Size::Small => (40, 40),
Size::Medium => (60, 60),
Size::Large => (75, 75),
Size::XLarge => (83, 83),
Size::XXLarge => (90, 90),
Size::Long => (65, 75),
}
+1
View File
@@ -18,6 +18,7 @@ mod tests {
assert_eq!(Size::Small.to_percent(), (40, 40));
assert_eq!(Size::Medium.to_percent(), (60, 60));
assert_eq!(Size::Large.to_percent(), (75, 75));
assert_eq!(Size::XLarge.to_percent(), (83, 83));
assert_eq!(Size::XXLarge.to_percent(), (90, 90));
assert_eq!(Size::Long.to_percent(), (65, 75));
}