feat(ui): Sonarr support for the series details popup

This commit is contained in:
2024-12-06 20:30:26 -07:00
parent 73d666d1f5
commit 23b1ca4371
39 changed files with 3075 additions and 956 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ impl DrawUi for EditSeriesUi {
}
_ if SERIES_DETAILS_BLOCKS.contains(&context) => {
draw_popup_over_ui::<SeriesDetailsUi>(f, app, area, draw_library, Size::Large);
draw_popup(f, app, draw_edit_series_prompt, Size::Medium);
draw_popup(f, app, draw_edit_series_prompt, Size::Long);
}
_ => (),
}
+181 -33
View File
@@ -12,11 +12,11 @@ mod tests {
use crate::ui::DrawUi;
use pretty_assertions::assert_eq;
use ratatui::widgets::{Cell, Row};
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::models::sonarr_models::{Season, SeasonStatistics};
use crate::{
models::sonarr_models::{Series, SeriesStatistics},
models::sonarr_models::Series,
ui::sonarr_ui::library::decorate_series_row_with_style,
};
@@ -38,45 +38,193 @@ mod tests {
});
}
#[rstest]
#[case(SeriesStatus::Ended, None, RowStyle::Missing)]
#[case(SeriesStatus::Ended, Some(59.0), RowStyle::Missing)]
#[case(SeriesStatus::Ended, Some(100.0), RowStyle::Downloaded)]
#[case(SeriesStatus::Continuing, None, RowStyle::Missing)]
#[case(SeriesStatus::Continuing, Some(59.0), RowStyle::Missing)]
#[case(SeriesStatus::Continuing, Some(100.0), RowStyle::Unreleased)]
#[case(SeriesStatus::Upcoming, None, RowStyle::Unreleased)]
#[case(SeriesStatus::Deleted, None, RowStyle::Missing)]
fn test_decorate_series_row_with_style(
#[case] series_status: SeriesStatus,
#[case] percent_of_episodes: Option<f64>,
#[case] expected_row_style: RowStyle,
#[test]
fn test_decorate_row_with_style_downloaded_when_ended_and_all_monitored_episodes_are_present(
) {
let mut series = Series {
status: series_status,
let seasons = vec![
Season {
monitored: false,
statistics: SeasonStatistics {
episode_count: 1,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
Season {
monitored: true,
statistics: SeasonStatistics {
episode_count: 3,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
];
let series = Series {
status: SeriesStatus::Ended,
seasons: Some(seasons),
..Series::default()
};
if let Some(percentage) = percent_of_episodes {
series.statistics = Some(SeriesStatistics {
percent_of_episodes: percentage,
..SeriesStatistics::default()
});
}
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
match expected_row_style {
RowStyle::Downloaded => assert_eq!(style, row.downloaded()),
RowStyle::Missing => assert_eq!(style, row.missing()),
RowStyle::Unreleased => assert_eq!(style, row.unreleased()),
}
assert_eq!(style, row.downloaded());
}
enum RowStyle {
Downloaded,
Missing,
Unreleased,
#[test]
fn test_decorate_row_with_style_missing_when_ended_and_episodes_are_missing() {
let seasons = vec![
Season {
monitored: true,
statistics: SeasonStatistics {
episode_count: 1,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
Season {
monitored: true,
statistics: SeasonStatistics {
episode_count: 3,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
];
let series = Series {
status: SeriesStatus::Ended,
seasons: Some(seasons),
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.missing());
}
#[test]
fn test_decorate_row_with_style_indeterminate_when_ended_and_seasons_is_empty() {
let series = Series {
status: SeriesStatus::Ended,
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.indeterminate());
}
#[test]
fn test_decorate_row_with_style_unreleased_when_continuing_and_all_monitored_episodes_are_present(
) {
let seasons = vec![
Season {
monitored: false,
statistics: SeasonStatistics {
episode_count: 1,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
Season {
monitored: true,
statistics: SeasonStatistics {
episode_count: 3,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
];
let series = Series {
status: SeriesStatus::Continuing,
seasons: Some(seasons),
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.unreleased());
}
#[test]
fn test_decorate_row_with_style_missing_when_continuing_and_episodes_are_missing() {
let seasons = vec![
Season {
monitored: true,
statistics: SeasonStatistics {
episode_count: 1,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
Season {
monitored: true,
statistics: SeasonStatistics {
episode_count: 3,
total_episode_count: 3,
..SeasonStatistics::default()
},
..Season::default()
},
];
let series = Series {
status: SeriesStatus::Continuing,
seasons: Some(seasons),
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.missing());
}
#[test]
fn test_decorate_row_with_style_indeterminate_when_continuing_and_seasons_is_empty() {
let series = Series {
status: SeriesStatus::Continuing,
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.indeterminate());
}
#[test]
fn test_decorate_row_with_style_unreleased_when_upcoming() {
let series = Series {
status: SeriesStatus::Upcoming,
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.unreleased());
}
#[test]
fn test_decorate_row_with_style_defaults_to_indeterminate() {
let series = Series {
status: SeriesStatus::Deleted,
..Series::default()
};
let row = Row::new(vec![Cell::from("test".to_owned())]);
let style = decorate_series_row_with_style(&series, row.clone());
assert_eq!(style, row.indeterminate());
}
}
+24 -41
View File
@@ -61,35 +61,6 @@ impl DrawUi for LibraryUi {
| ActiveSonarrBlock::SearchSeriesError
| ActiveSonarrBlock::FilterSeries
| ActiveSonarrBlock::FilterSeriesError => draw_library(f, app, area),
// ActiveSonarrBlock::SearchSeries => draw_popup_over(
// f,
// app,
// area,
// draw_library,
// draw_library_search_box,
// Size::InputBox,
// ),
// ActiveSonarrBlock::SearchSeriesError => {
// let popup = Popup::new(Message::new("Series not found!")).size(Size::Message);
// draw_library(f, app, area);
// f.render_widget(popup, f.area());
// }
// ActiveSonarrBlock::FilterSeries => draw_popup_over(
// f,
// app,
// area,
// draw_library,
// draw_filter_series_box,
// Size::InputBox,
// ),
// ActiveSonarrBlock::FilterSeriesError => {
// let popup = Popup::new(Message::new("No series found matching the given filter!"))
// .size(Size::Message);
// draw_library(f, app, area);
// f.render_widget(popup, f.area());
// }
ActiveSonarrBlock::UpdateAllSeriesPrompt => {
let confirmation_prompt = ConfirmationPrompt::new()
.title("Update All Series")
@@ -234,24 +205,36 @@ pub(super) fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
fn decorate_series_row_with_style<'a>(series: &Series, row: Row<'a>) -> Row<'a> {
match series.status {
SeriesStatus::Ended => {
if let Some(ref stats) = series.statistics {
if stats.percent_of_episodes == 100.0 {
return row.downloaded();
if let Some(ref seasons) = series.seasons {
return if seasons
.iter()
.filter(|season| season.monitored)
.all(|season| season.statistics.episode_count == season.statistics.total_episode_count)
{
row.downloaded()
} else {
row.missing()
}
}
row.missing()
}
row.indeterminate()
}
SeriesStatus::Continuing => {
if let Some(ref stats) = series.statistics {
if stats.percent_of_episodes == 100.0 {
return row.unreleased();
}
if let Some(ref seasons) = series.seasons {
return if seasons
.iter()
.filter(|season| season.monitored)
.all(|season| season.statistics.episode_count == season.statistics.total_episode_count)
{
row.unreleased()
} else {
row.missing()
};
}
row.missing()
row.indeterminate()
}
SeriesStatus::Upcoming => row.unreleased(),
_ => row.missing(),
_ => row.indeterminate(),
}
}
+35 -19
View File
@@ -1,8 +1,10 @@
use deunicode::deunicode;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Text};
use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
use ratatui::Frame;
use regex::Regex;
use crate::app::App;
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SERIES_DETAILS_BLOCKS};
@@ -67,6 +69,20 @@ impl DrawUi for SeriesDetailsUi {
draw_series_details(f, app, content_area);
match active_sonarr_block {
ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => {
let prompt = format!(
"Do you want to trigger an automatic search of your indexers for all monitored episode(s) for the series: {}", app.data.sonarr_data.series.current_selection().title
);
let confirmation_prompt = ConfirmationPrompt::new()
.title("Automatic Series Search")
.prompt(&prompt)
.yes_no_value(app.data.sonarr_data.prompt_confirm);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::MediumPrompt),
f.area(),
);
}
ActiveSonarrBlock::UpdateAndScanSeriesPrompt => {
let prompt = format!(
"Do you want to trigger an update and disk scan for the series: {}?",
@@ -83,14 +99,7 @@ impl DrawUi for SeriesDetailsUi {
);
}
ActiveSonarrBlock::SeriesHistoryDetails => {
draw_popup_over(
f,
app,
popup_area,
draw_series_history_table,
draw_history_item_details_popup,
Size::Small,
);
draw_history_item_details_popup(f, app, popup_area);
}
_ => (),
};
@@ -129,19 +138,25 @@ pub fn draw_series_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
.get_by_left(&current_selection.language_profile_id)
.unwrap()
.to_owned();
let overview = Regex::new(r"[\r\n\t]")
.unwrap()
.replace_all(
&deunicode(
current_selection
.overview
.as_ref()
.unwrap_or(&String::new()),
),
"",
)
.to_string();
let mut series_description = vec![
Line::from(vec![
"Title: ".primary().bold(),
current_selection.title.text.clone().primary().bold(),
]),
Line::from(vec![
"Overview: ".primary().bold(),
current_selection
.overview
.clone()
.unwrap_or_default()
.default(),
]),
Line::from(vec!["Overview: ".primary().bold(), overview.default()]),
Line::from(vec![
"Network: ".primary().bold(),
current_selection
@@ -194,7 +209,7 @@ pub fn draw_series_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
let description_paragraph = Paragraph::new(series_description)
.block(borderless_block())
.wrap(Wrap { trim: false });
.wrap(Wrap { trim: true });
f.render_widget(description_paragraph, area);
}
@@ -220,9 +235,10 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.get_active_tab_contextual_help();
let season_row_mapping = |season: &Season| {
let Season {
season_number,
title,
monitored,
statistics,
..
} = season;
let SeasonStatistics {
episode_count,
@@ -235,7 +251,7 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let row = Row::new(vec![
Cell::from(season_monitored.to_owned()),
Cell::from(format!("Season {}", season_number)),
Cell::from(title.clone().unwrap()),
Cell::from(format!("{}/{}", episode_count, total_episode_count)),
Cell::from(format!("{size:.2} GB")),
]);