use add_series_ui::AddSeriesUi; use delete_series_ui::DeleteSeriesUi; use edit_series_ui::EditSeriesUi; use ratatui::{ Frame, layout::{Constraint, Rect}, widgets::{Cell, Row}, }; use series_details_ui::SeriesDetailsUi; use crate::ui::widgets::{ confirmation_prompt::ConfirmationPrompt, popup::{Popup, Size}, }; use crate::utils::convert_to_gb; use crate::{ app::App, models::{ Route, servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, LIBRARY_BLOCKS}, sonarr_models::{Series, SeriesStatus}, }, ui::{ DrawUi, styles::ManagarrStyle, utils::{get_width_from_percentage, layout_block_top_border}, widgets::managarr_table::ManagarrTable, }, }; mod add_series_ui; mod delete_series_ui; mod edit_series_ui; mod series_details_ui; mod episode_details_ui; #[cfg(test)] #[path = "library_ui_tests.rs"] mod library_ui_tests; mod season_details_ui; pub(super) struct LibraryUi; impl DrawUi for LibraryUi { fn accepts(route: Route) -> bool { if let Route::Sonarr(active_sonarr_block, _) = route { return AddSeriesUi::accepts(route) || DeleteSeriesUi::accepts(route) || EditSeriesUi::accepts(route) || SeriesDetailsUi::accepts(route) || LIBRARY_BLOCKS.contains(&active_sonarr_block); } false } fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let route = app.get_current_route(); draw_library(f, app, area); match route { _ if AddSeriesUi::accepts(route) => AddSeriesUi::draw(f, app, area), _ if DeleteSeriesUi::accepts(route) => DeleteSeriesUi::draw(f, app, area), _ if EditSeriesUi::accepts(route) => EditSeriesUi::draw(f, app, area), _ if SeriesDetailsUi::accepts(route) => SeriesDetailsUi::draw(f, app, area), Route::Sonarr(ActiveSonarrBlock::UpdateAllSeriesPrompt, _) => { let confirmation_prompt = ConfirmationPrompt::new() .title("Update All Series") .prompt("Do you want to update info and scan your disks for all of your series?") .yes_no_value(app.data.sonarr_data.prompt_confirm); f.render_widget( Popup::new(confirmation_prompt).size(Size::MediumPrompt), f.area(), ); } _ => (), } } } fn draw_library(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.series.items.is_empty() { app.data.sonarr_data.series.current_selection().clone() } else { Series::default() }; let quality_profile_map = app.data.sonarr_data.quality_profile_map.clone(); let language_profile_map = app.data.sonarr_data.language_profiles_map.clone(); let tags_map = app.data.sonarr_data.tags_map.clone(); let ui_scroll_tick_count = app.ui_scroll_tick_count; let series_table_row_mapping = |series: &Series| { series.title.scroll_left_or_reset( get_width_from_percentage(area, 23), *series == current_selection, ui_scroll_tick_count == 0, ); let monitored = if series.monitored { "🏷" } else { "" }; let certification = series.certification.clone().unwrap_or_default(); let network = series.network.clone().unwrap_or_default(); let size = series .statistics .as_ref() .map_or(0f64, |stats| convert_to_gb(stats.size_on_disk)); let quality_profile = quality_profile_map .get_by_left(&series.quality_profile_id) .expect("Quality profile ID must exist in quality_profile_map") .to_owned(); let language_profile = language_profile_map .get_by_left(&series.language_profile_id) .expect("Language profile ID must exist in language_profile_map") .to_owned(); let tags = series .tags .iter() .filter_map(|tag_id| { let id = tag_id.as_i64()?; tags_map.get_by_left(&id).cloned() }) .collect::>() .join(", "); decorate_series_row_with_style( series, Row::new(vec![ Cell::from(series.title.to_string()), Cell::from(series.year.to_string()), Cell::from(network), Cell::from(series.status.to_display_str()), Cell::from(certification), Cell::from(series.series_type.to_display_str()), Cell::from(quality_profile), Cell::from(language_profile), Cell::from(format!("{size:.2} GB")), Cell::from(monitored.to_owned()), Cell::from(tags), ]), ) }; let series_table = ManagarrTable::new( app, |app| Some(&mut app.data.sonarr_data.series), series_table_row_mapping, ) .block(layout_block_top_border()) .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesSortPrompt) .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeries) .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeries) .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeriesError) .filter_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::FilterSeriesError) .headers([ "Title", "Year", "Network", "Status", "Rating", "Type", "Quality Profile", "Language", "Size", "Monitored", "Tags", ]) .constraints([ Constraint::Percentage(20), Constraint::Percentage(4), Constraint::Percentage(14), Constraint::Percentage(6), Constraint::Percentage(6), Constraint::Percentage(6), Constraint::Percentage(11), Constraint::Percentage(8), Constraint::Percentage(7), Constraint::Percentage(6), Constraint::Percentage(12), ]); if [ ActiveSonarrBlock::SearchSeries, ActiveSonarrBlock::FilterSeries, ] .contains(&active_sonarr_block) { series_table.show_cursor(f, area); } f.render_widget(series_table, area); } } fn decorate_series_row_with_style<'a>(series: &Series, row: Row<'a>) -> Row<'a> { if !series.monitored { return row.unmonitored(); } match series.status { SeriesStatus::Ended => { if let Some(ref seasons) = series.seasons { return if seasons .iter() .filter(|season| season.monitored) .filter(|season| season.statistics.is_some()) .all(|season| { season .statistics .as_ref() .expect("Season Statistics is undefined") .episode_file_count == season .statistics .as_ref() .expect("Season statistics is undefined") .episode_count }) { row.downloaded() } else { row.missing() }; } row.indeterminate() } SeriesStatus::Continuing => { if let Some(ref seasons) = series.seasons { return if seasons .iter() .filter(|season| season.monitored) .filter(|season| season.statistics.is_some()) .all(|season| { season .statistics .as_ref() .expect("Season Statistics is undefined") .episode_file_count == season .statistics .as_ref() .expect("Season statistics is undefined") .episode_count }) { row.unreleased() } else { row.missing() }; } row.indeterminate() } SeriesStatus::Upcoming => row.unreleased(), _ => row.indeterminate(), } }